packages feed

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 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