turtle 1.1.1 → 1.2.0
raw patch · 7 files changed
+548/−36 lines, 7 filesdep +hostnamedep +optional-argsdep +optparse-applicativedep ~basedep ~clockdep ~directory
Dependencies added: hostname, optional-args, optparse-applicative
Dependency ranges changed: base, clock, directory, foldl
Files
- src/Turtle.hs +2/−0
- src/Turtle/Format.hs +1/−6
- src/Turtle/Options.hs +208/−0
- src/Turtle/Pattern.hs +13/−0
- src/Turtle/Prelude.hs +162/−20
- src/Turtle/Tutorial.hs +153/−5
- turtle.cabal +9/−5
src/Turtle.hs view
@@ -68,6 +68,7 @@ -- * Modules module Turtle.Format , module Turtle.Pattern + , module Turtle.Options , module Turtle.Shell , module Turtle.Prelude , module Control.Applicative @@ -89,6 +90,7 @@ import Turtle.Format import Turtle.Pattern +import Turtle.Options import Turtle.Shell import Turtle.Prelude import Control.Applicative
src/Turtle/Format.hs view
@@ -180,12 +180,7 @@ s :: Format r (Text -> r) s = makeFormat id -{-| `Format` a `Filesystem.Path.CurrentOS.FilePath` into `Text` - ->>> import Filesystem.Path.CurrentOS((</>)) ->>> format fp ("usr" </> "lib") -"usr/lib" --} +-- | `Format` a `Filesystem.Path.CurrentOS.FilePath` into `Text` fp :: Format r (FilePath -> r) fp = makeFormat (\fpath -> either id id (toText fpath))
+ src/Turtle/Options.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- | Example usage of this module: +-- +-- > -- options.hs +-- > +-- > {-# LANGUAGE OverloadedStrings #-} +-- > +-- > import Turtle +-- > +-- > parser :: Parser (Text, Int) +-- > parser = (,) <$> optText "name" 'n' "Your first name" +-- > <*> optInt "age" 'a' "Your current age" +-- > +-- > main = do +-- > (name, age) <- options "Greeting script" parser +-- > echo (format ("Hello there, "%s) name) +-- > echo (format ("You are "%d%" years old") age) +-- +-- > $ ./options --name John --age 42 +-- > Hello there, John +-- > You are 42 years old +-- +-- > $ ./options --help +-- > Greeting script +-- > +-- > Usage: options (-n|--name NAME) (-a|--age AGE) +-- > +-- > Available options: +-- > -h,--help Show this help text +-- > --name NAME Your first name +-- > --age AGE Your current age + +module Turtle.Options + ( -- * Types + Parser + , ArgName + , ShortName + , Description + , HelpMessage + + -- * Flag-based option parsers + , switch + , optText + , optInt + , optInteger + , optDouble + , optPath + , optRead + , opt + + -- * Positional argument parsers + , argText + , argInt + , argInteger + , argDouble + , argPath + , argRead + , arg + + -- * Consume parsers + , options + + ) where + +import Data.Monoid +import Data.Foldable +import Data.String (IsString) +import Text.Read (readMaybe) +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Optional +import Control.Applicative +import Control.Monad.IO.Class +import Filesystem.Path.CurrentOS (FilePath, fromText) +import Options.Applicative (Parser) +import qualified Options.Applicative as Opts +import qualified Options.Applicative.Types as Opts +import Prelude hiding (FilePath) + +-- | Parse the given options from the command line +options :: MonadIO io => Description -> Parser a -> io a +options desc parser = liftIO + $ Opts.execParser + $ Opts.info (Opts.helper <*> parser) + (Opts.header (Text.unpack (getDescription desc))) + +{-| The name of a command-line argument + + This is used to infer the long name and metavariable for the command line + flag. For example, an `ArgName` of @\"name\"@ will create a @--name@ flag + with a @NAME@ metavariable +-} +newtype ArgName = ArgName { getArgName :: Text } + deriving (IsString) + +-- | The short one-character abbreviation for a flag (i.e. @-n@) +type ShortName = Char + +{-| A brief description of what your program does + + This description will appear in the header of the @--help@ output +-} +newtype Description = Description { getDescription :: Text } + deriving (IsString) + +{-| A helpful message explaining what a flag does + + This will appear in the @--help@ output +-} +newtype HelpMessage = HelpMessage { getHelpMessage :: Text } + deriving (IsString) + +{-| This parser returns `True` if the given flag is set and `False` if the + flag is absent +-} +switch + :: ArgName + -> ShortName + -> Optional HelpMessage + -> Parser Bool +switch argName c helpMessage + = Opts.switch + $ (Opts.long . Text.unpack . getArgName) argName + <> Opts.short c + <> foldMap (Opts.help . Text.unpack . getHelpMessage) helpMessage + +{- | Build a flag-based option parser for any type by providing a `Text`-parsing + function +-} +opt :: (Text -> Maybe a) + -> ArgName + -> ShortName + -> Optional HelpMessage + -> Parser a +opt argParse argName c helpMessage + = Opts.option (argParseToReadM argParse) + $ Opts.metavar (Text.unpack (Text.toUpper (getArgName argName))) + <> Opts.long (Text.unpack (getArgName argName)) + <> Opts.short c + <> foldMap (Opts.help . Text.unpack . getHelpMessage) helpMessage + +-- | Parse any type that implements `Read` +optRead :: Read a => ArgName -> ShortName -> Optional HelpMessage -> Parser a +optRead = opt (readMaybe . Text.unpack) + +-- | Parse an `Int` as a flag-based option +optInt :: ArgName -> ShortName -> Optional HelpMessage -> Parser Int +optInt = optRead + +-- | Parse an `Integer` as a flag-based option +optInteger :: ArgName -> ShortName -> Optional HelpMessage -> Parser Integer +optInteger = optRead + +-- | Parse a `Double` as a flag-based option +optDouble :: ArgName -> ShortName -> Optional HelpMessage -> Parser Double +optDouble = optRead + +-- | Parse a `Text` value as a flag-based option +optText :: ArgName -> ShortName -> Optional HelpMessage -> Parser Text +optText = opt Just + +-- | Parse a `FilePath` value as a flag-based option +optPath :: ArgName -> ShortName -> Optional HelpMessage -> Parser FilePath +optPath argName short msg = fmap fromText (optText argName short msg) + +{- | Build a positional argument parser for any type by providing a + `Text`-parsing function +-} +arg :: (Text -> Maybe a) + -> ArgName + -> Optional HelpMessage + -> Parser a +arg argParse argName helpMessage + = Opts.argument (argParseToReadM argParse) + $ Opts.metavar (Text.unpack (Text.toUpper (getArgName argName))) + <> foldMap (Opts.help . Text.unpack . getHelpMessage) helpMessage + +-- | Parse any type that implements `Read` as a positional argument +argRead :: Read a => ArgName -> Optional HelpMessage -> Parser a +argRead = arg (readMaybe . Text.unpack) + +-- | Parse an `Int` as a positional argument +argInt :: ArgName -> Optional HelpMessage -> Parser Int +argInt = argRead + +-- | Parse an `Integer` as a positional argument +argInteger :: ArgName -> Optional HelpMessage -> Parser Integer +argInteger = argRead + +-- | Parse a `Double` as a positional argument +argDouble :: ArgName -> Optional HelpMessage -> Parser Double +argDouble = argRead + +-- | Parse a `Text` as a positional argument +argText :: ArgName -> Optional HelpMessage -> Parser Text +argText = arg Just + +-- | Parse a `FilePath` as a positional argument +argPath :: ArgName -> Optional HelpMessage -> Parser FilePath +argPath argName msg = fmap fromText (argText argName msg) + +argParseToReadM :: (Text -> Maybe a) -> Opts.ReadM a +argParseToReadM f = do + s <- Opts.readerAsk + case f (Text.pack s) of + Just a -> return a + Nothing -> Opts.readerAbort Opts.ShowHelpText
src/Turtle/Pattern.hs view
@@ -81,6 +81,7 @@ , prefix , suffix , has + , invert , once , star , plus @@ -425,6 +426,18 @@ signed p = do sign <- (char '+' *> pure id) <|> (char '-' *> pure negate) <|> (pure id) fmap sign p + +{-| @(`invert` p)@ succeeds if @p@ fails and fails if @p@ succeeds + +>>> match (invert "A") "A" +[] +>>> match (invert "A") "B" +[()] +-} +invert :: Pattern a -> Pattern () +invert p = Pattern (StateT (\str -> case runStateT (runPattern p) str of + [] -> [((), "")] + _ -> [] )) {-| Match a `Char`, but return `Text`
src/Turtle/Prelude.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE CPP #-} -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | This module provides a large suite of utilities that resemble Unix -- utilities. @@ -123,13 +124,13 @@ , rm , rmdir , rmtree - , du , testfile , testdir , date , datefile , touch , time + , hostname , sleep , exit , die @@ -152,9 +153,10 @@ , input , inhandle , stdout - , stderr , output + , outhandle , append + , stderr , strict , ls , lstree @@ -165,7 +167,13 @@ , yes , limit , limitWhile + , cache + -- * Folds + , countChars + , countWords + , countLines + -- * Permissions , Permissions , chmod @@ -176,13 +184,27 @@ , executable, nonexecutable , searchable, nonsearchable , ooo,roo,owo,oox,oos,rwo,rox,ros,owx,rwx,rws + + -- * File size + , du + , Size + , bytes + , kilobytes + , megabytes + , gigabytes + , terabytes + , kibibytes + , mebibytes + , gibibytes + , tebibytes ) where -import Control.Applicative (Alternative(..)) +import Control.Applicative (Alternative(..), (<*), (*>)) import Control.Concurrent.Async (Async, withAsync, wait, concurrently) import Control.Concurrent (threadDelay) import Control.Exception (bracket, throwIO) -import Control.Foldl (FoldM(..), list) +import Control.Foldl (Fold, FoldM(..), genericLength, handles, list, premap) +import qualified Control.Foldl.Text import Control.Monad (liftM, msum, when) import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.Managed (Managed, managed) @@ -192,11 +214,13 @@ import Data.IORef (newIORef, readIORef, writeIORef) import Data.Text (Text, pack, unpack) import Data.Time (NominalDiffTime, UTCTime, getCurrentTime) +import Data.Traversable (traverse) import qualified Data.Text as Text import qualified Data.Text.IO as Text import qualified Filesystem import Filesystem.Path.CurrentOS (FilePath, (</>)) import qualified Filesystem.Path.CurrentOS as Filesystem +import Network.HostName (getHostName) import System.Clock (Clock(..), TimeSpec(..), getTime) import System.Environment ( getArgs, @@ -224,6 +248,7 @@ import Turtle.Pattern (Pattern, anyChar, match) import Turtle.Shell +import Turtle.Format (format, w, (%)) {-| Run a command using @execvp@, retrieving the exit code @@ -312,7 +337,7 @@ let feedIn = sh (do txt <- s liftIO (Text.hPutStrLn hIn txt) ) - withAsync feedIn (\_ -> liftIO (Process.waitForProcess ph) ) ) + withAsync feedIn (\a -> liftIO (Process.waitForProcess ph) <* wait a) ) systemStrict :: MonadIO io @@ -333,7 +358,7 @@ txt <- s liftIO (Text.hPutStrLn hIn txt) ) concurrently - (withAsync feedIn (\_ -> liftIO (Process.waitForProcess ph) )) + (withAsync feedIn (\a -> liftIO (Process.waitForProcess ph) <* wait a)) (Text.hGetContents hOut) ) {-| Run a command using @execvp@, streaming @stdout@ as lines of `Text` @@ -384,8 +409,8 @@ let feedIn = sh (do txt <- s liftIO (Text.hPutStrLn hIn txt) ) - _ <- using (fork feedIn) - inhandle hOut + a <- using (fork feedIn) + inhandle hOut <|> (liftIO (wait a) *> empty) -- | Print to @stdout@ echo :: MonadIO io => Text -> io () @@ -557,10 +582,6 @@ rmtree :: MonadIO io => FilePath -> io () rmtree path = liftIO (Filesystem.removeTree path) --- | Get a file or directory's size -du :: MonadIO io => FilePath -> io Integer -du path = liftIO (Filesystem.getSize path) - -- | Check if a file exists testfile :: MonadIO io => FilePath -> io Bool testfile path = liftIO (Filesystem.isFile path) @@ -717,6 +738,10 @@ + fromIntegral (nanoseconds2 - nanoseconds1) / 10^(9::Int) return (a, fromRational t) +-- | Get the system's host name +hostname :: MonadIO io => io Text +hostname = liftIO (fmap Text.pack getHostName) + {-| Sleep for the given duration A numeric literal argument is interpreted as seconds. In other words, @@ -828,12 +853,6 @@ txt <- s liftIO (echo txt) ) --- | Stream lines of `Text` to standard error -stderr :: MonadIO io => Shell Text -> io () -stderr s = sh (do - txt <- s - liftIO (err txt) ) - -- | Stream lines of `Text` to a file output :: MonadIO io => FilePath -> Shell Text -> io () output file s = sh (do @@ -841,6 +860,12 @@ txt <- s liftIO (Text.hPutStrLn handle txt) ) +-- | Stream lines of `Text` to a `Handle` +outhandle :: MonadIO io => Handle -> Shell Text -> io () +outhandle handle s = sh (do + txt <- s + liftIO (Text.hPutStrLn handle txt) ) + -- | Stream lines of `Text` to append to a file append :: MonadIO io => FilePath -> Shell Text -> io () append file s = sh (do @@ -848,6 +873,12 @@ txt <- s liftIO (Text.hPutStrLn handle txt) ) +-- | Stream lines of `Text` to standard error +stderr :: MonadIO io => Shell Text -> io () +stderr s = sh (do + txt <- s + liftIO (err txt) ) + -- | Read in a stream's contents strictly strict :: MonadIO io => Shell Text -> io Text strict s = liftM Text.unlines (fold s list) @@ -930,6 +961,39 @@ if b' then step x a else return x foldIO s (FoldM step' begin done) ) +{-| Cache a `Shell`'s output so that repeated runs of the script will reuse the + result of previous runs. You must supply a `FilePath` where the cached + result will be stored. + + The stored result is only reused if the `Shell` successfully ran to + completion without any exceptions. Note: on some platforms Ctrl-C will + flush standard input and signal end of file before killing the program, + which may trick the program into \"successfully\" completing. +-} +cache :: (Read a, Show a) => FilePath -> Shell a -> Shell a +cache file s = do + let cached = do + txt <- input file + case reads (Text.unpack txt) of + [(ma, "")] -> return ma + _ -> + die (format ("cache: Invalid data stored in "%w) file) + exists <- testfile file + mas <- fold (if exists then cached else empty) list + case [ () | Nothing <- mas ] of + _:_ -> select [ a | Just a <- mas ] + _ -> do + handle <- using (writeonly file) + let justs = do + a <- s + liftIO (Text.hPutStrLn handle (Text.pack (show (Just a)))) + return a + let nothing = do + let n = Nothing :: Maybe () + liftIO (Text.hPutStrLn handle (Text.pack (show n))) + empty + justs <|> nothing + -- | Get the current time date :: MonadIO io => io UTCTime date = liftIO getCurrentTime @@ -937,3 +1001,81 @@ -- | Get the time a file was last modified datefile :: MonadIO io => FilePath -> io UTCTime datefile path = liftIO (Filesystem.getModified path) + +-- | Get the size of a file or a directory +du :: MonadIO io => FilePath -> io Size +du path = liftIO (fmap Size (Filesystem.getSize path)) + +{-| An abstract file size + + Specify the units you want by using an accessor like `kilobytes` + + The `Num` instance for `Size` interprets numeric literals as bytes +-} +newtype Size = Size { _bytes :: Integer } deriving (Num) + +instance Show Size where + show = show . _bytes + +-- | Extract a size in bytes +bytes :: Integral n => Size -> n +bytes = fromInteger . _bytes + +-- | @1 kilobyte = 1000 bytes@ +kilobytes :: Integral n => Size -> n +kilobytes = (`div` 1000) . bytes + +-- | @1 megabyte = 1000 kilobytes@ +megabytes :: Integral n => Size -> n +megabytes = (`div` 1000) . kilobytes + +-- | @1 gigabyte = 1000 megabytes@ +gigabytes :: Integral n => Size -> n +gigabytes = (`div` 1000) . megabytes + +-- | @1 terabyte = 1000 gigabytes@ +terabytes :: Integral n => Size -> n +terabytes = (`div` 1000) . gigabytes + +-- | @1 kibibyte = 1024 bytes@ +kibibytes :: Integral n => Size -> n +kibibytes = (`div` 1024) . bytes + +-- | @1 mebibyte = 1024 kibibytes@ +mebibytes :: Integral n => Size -> n +mebibytes = (`div` 1024) . kibibytes + +-- | @1 gibibyte = 1024 mebibytes@ +gibibytes :: Integral n => Size -> n +gibibytes = (`div` 1024) . mebibytes + +-- | @1 tebibyte = 1024 gibibytes@ +tebibytes :: Integral n => Size -> n +tebibytes = (`div` 1024) . gibibytes + +{-| Count the number of characters in the stream (like @wc -c@) + + This uses the convention that the elements of the stream are implicitly + ended by newlines that are one character wide +-} +countChars :: Integral n => Fold Text n +countChars = Control.Foldl.Text.length + charsPerNewline * countLines + +charsPerNewline :: Num a => a +#ifdef mingw32_HOST_OS +charsPerNewline = 2 +#else +charsPerNewline = 1 +#endif + +-- | Count the number of words in the stream (like @wc -w@) +countWords :: Integral n => Fold Text n +countWords = premap Text.words (handles traverse genericLength) + +{-| Count the number of lines in the stream (like @wc -l@) + + This uses the convention that each element of the stream represents one + line +-} +countLines :: Integral n => Fold Text n +countLines = genericLength
src/Turtle/Tutorial.hs view
@@ -82,6 +82,9 @@ -- * MonadIO -- $monadio + -- * Command line options + -- $cmdline + -- * Conclusion -- $conclusion ) where @@ -1398,6 +1401,156 @@ -- way (such as `print`), so you will still occasionally need to wrap -- subroutines in `liftIO`. +-- $cmdline +-- +-- The "Turtle.Options" module lets you easily parse command line arguments, +-- using either flags or positional arguments. +-- +-- For example, if you want to write a @cp@-like script that takes two +-- positional arguments for the source and destination file, you can write: +-- +-- > #!/usr/bin/env runhaskell +-- > +-- > -- cp.hs +-- > +-- > {-# LANGUAGE OverloadedStrings #-} +-- > +-- > import Turtle +-- > import Prelude hiding (FilePath) +-- > +-- > parser :: Parser (FilePath, FilePath) +-- > parser = (,) <$> argPath "src" "The source file" +-- > <*> argPath "dest" "The destination file" +-- > +-- > main = do +-- > (src, dest) <- options "A simple `cp` script" parser +-- > cp src dest +-- +-- If you run the script without any arguments, you will get an auto-generated +-- usage output: +-- +-- > $ ./cp.hs +-- > Usage: cp.hs SRC DEST +-- +-- ... and you can get a more descriptive output if you supply the @--help@ +-- flag: +-- +-- > $ ./cp.hs --help +-- > A simple `cp` utility +-- > +-- > Usage: cp.hs SRC DEST +-- > +-- > Available options: +-- > -h,--help Show this help text +-- > SRC The source file +-- > DEST The destination file +-- +-- ... and the script works as expected if you provide both arguments: +-- +-- > echo "Test" > file1.txt +-- > $ ./cp.hs file1.txt file2.txt +-- > cat file2.txt +-- +-- This works because `argPath` produces a `Parser`: +-- +-- > argPath :: ArgName -> Optional HelpMessage -> Parser FilePath +-- +-- ... and multiple `Parser`s can be combined into a single `Parser` using +-- operations from the `Applicative` type class since the `Parser` type +-- implements the `Applicative` interface: +-- +-- > instance Applicative Parser +-- +-- You can also make any argument optional using the `optional` utility +-- provided by `Control.Applicative`: +-- +-- @ +-- `optional` :: `Alternative` f => f a -> f (Maybe a) +-- @ +-- +-- For example, we can change our program to make the destination argument +-- optional, defaulting to `stdout` if the user does not provide a destination: +-- +-- > {-# LANGUAGE OverloadedStrings #-} +-- > +-- > import Turtle +-- > import Prelude hiding (FilePath) +-- > +-- > parser :: Parser (FilePath, FilePath) +-- > parser = (,) <$> argPath "src" "The source file" +-- > <*> argPath "dest" "The destination file" +-- > +-- > main = do +-- > (src, dest) <- options "A simple `cp` utility" parser +-- > cp src dest +-- +-- Now the auto-generated usage information correctly indicates that the second +-- argument is optional: +-- +-- > $ ./cp.hs +-- > Usage: cp.hs SRC [DEST] +-- > $ ./cp.hs --help +-- > A simple `cp` utility +-- > +-- > Usage: cp.hs SRC [DEST] +-- > +-- > Available options: +-- > -h,--help Show this help text +-- > SRC The source file +-- > DEST The destination file +-- +-- ... and if we omit the argument the result goes to standard output: +-- +-- > $ ./cp.hs file1.txt +-- > Test +-- +-- We can use the `optional` utility because the `Parser` type also implements +-- the `Alternative` interface: +-- +-- > instance Alternative Parser +-- +-- We can also specify arguments on the command lines using flags instead of +-- specifying them positionally. Let's change our example to specify the +-- input and output using the @--src@ and @--dest@ flags, using @-s@ and @-d@ +-- as short-hands for the flags: +-- +-- > #!/usr/bin/env runhaskell +-- > +-- > {-# LANGUAGE OverloadedStrings #-} +-- > +-- > import Turtle +-- > import Prelude hiding (FilePath) +-- > +-- > parser :: Parser (FilePath, FilePath) +-- > parser = (,) <$> optPath "src" 's' "The source file" +-- > <*> optPath "dest" 'd' "The destination file" +-- > +-- > main = do +-- > (src, dest) <- options "A simple `cp` utility" parser +-- > cp src dest +-- +-- This now lets us specify the arguments in terms of flags: +-- +-- > $ ./cp +-- > Usage: cp.hs (-s|--src SRC) (-d|--dest DEST) +-- > $ ./cp --help +-- > A simple `cp` utility +-- > +-- > Usage: cp.hs (-s|--src SRC) (-d|--dest DEST) +-- > +-- > Available options: +-- > -h,--help Show this help text +-- > -s,--src SRC The source file +-- > -d,--dest DEST The destination file +-- > $ ./cp --src file1.txt --dest file3.txt +-- > $ cat file3.txt +-- > Test +-- +-- See the "Turtle.Options" module for more details and utilities related to +-- parsing command line options. This module is built on top of the +-- @optparse-applicative@ library, which provides even more extensive +-- functionality. + -- $conclusion -- -- By this point you should be able to write basic shell scripts in Haskell. If @@ -1424,8 +1577,3 @@ -- This library provides an extended suite of Unix-like utilities, but would -- still benefit from adding more utilities for better parity with the Unix -- ecosystem. Pull requests to add new utilities are highly welcome! --- --- The @turtle@ library does not yet provide support for command line argument --- parsing, but I highly recommend the @optparse-applicative@ library for this --- purpose. A future release of this library might include a simplified --- interface to @optparse-applicative@.
turtle.cabal view
@@ -1,5 +1,5 @@ Name: turtle -Version: 1.1.1 +Version: 1.2.0 Cabal-Version: >=1.10 Build-Type: Simple License: BSD3 @@ -48,9 +48,10 @@ Build-Depends: base >= 4.5 && < 5 , async >= 2.0.0.0 && < 2.1, - clock >= 0.4.1.2 && < 0.5, - directory < 1.3, - foldl < 1.2, + clock >= 0.4.1.2 && < 0.6, + directory >= 1.0.7 && < 1.3, + foldl >= 1.1 && < 1.2, + hostname < 1.1, managed < 1.1, process >= 1.0.1.1 && < 1.3, system-filepath >= 0.3.1 && < 0.5, @@ -58,7 +59,9 @@ temporary < 1.3, text < 1.3, time < 1.6, - transformers >= 0.2.0.0 && < 0.5 + transformers >= 0.2.0.0 && < 0.5, + optparse-applicative >= 0.11 && < 0.12, + optional-args >= 1.0 && < 2.0 if os(windows) Build-Depends: Win32 >= 2.2.0.1 && < 2.4 else @@ -68,6 +71,7 @@ Turtle.Format, Turtle.Pattern, Turtle.Shell, + Turtle.Options, Turtle.Prelude, Turtle.Tutorial GHC-Options: -O2 -Wall