turtle 1.2.0 → 1.2.1
raw patch · 12 files changed
+4314/−4244 lines, 12 filessetup-changed
Files
- LICENSE +24/−24
- Setup.hs +2/−2
- bench/Main.hs +40/−40
- src/Turtle.hs +161/−161
- src/Turtle/Format.hs +195/−195
- src/Turtle/Options.hs +208/−208
- src/Turtle/Pattern.hs +675/−675
- src/Turtle/Prelude.hs +1126/−1081
- src/Turtle/Shell.hs +173/−173
- src/Turtle/Tutorial.hs +1604/−1579
- test/Main.hs +6/−6
- turtle.cabal +100/−100
LICENSE view
@@ -1,24 +1,24 @@-Copyright (c) 2015 Gabriel Gonzalez -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of Gabriel Gonzalez nor the names of other contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright (c) 2015 Gabriel Gonzalez+All rights reserved.++Redistribution and use in source and binary forms, with or without modification,+are permitted provided that the following conditions are met:+ * Redistributions of source code must retain the above copyright notice,+ this list of conditions and the following disclaimer.+ * Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.+ * Neither the name of Gabriel Gonzalez nor the names of other contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Setup.hs view
@@ -1,2 +1,2 @@-import Distribution.Simple -main = defaultMain +import Distribution.Simple+main = defaultMain
bench/Main.hs view
@@ -1,40 +1,40 @@-{-# LANGUAGE OverloadedStrings #-} -module Main where - -import qualified Data.Text as Text -import Criterion.Main -import Turtle - -boundedNaive :: Int -> Int -> Pattern a -> Pattern [a] -boundedNaive m n p = do - x <- choice (map pure [m..n]) - count x p - -main :: IO () -main = defaultMain - [ bgroup "Pattern" - [ let cats = Text.replicate 1000 "cat" - furniture = Text.replicate 5 " " - in bgroup "Cat Lady's House" - [ bench "Basic" - $ nf (match (many "cat")) cats - , bench "Letters" - $ nf (match (many (mconcat ["c", "a", "t"]))) cats - , bench "Spaces" - $ nf (match (many "cat" <* spaces)) (cats <> furniture) - , bench "Prefix" - $ nf (match (prefix (many "cat"))) (cats <> furniture) - ] - , let hearts n = Text.replicate n "heart" - in bgroup "Love Knows No Bounds" - [ bench "500-700:650 Naive" - $ nf (match (boundedNaive 500 700 "heart")) (hearts 650) - , bench "500-700:650" - $ nf (match (bounded 500 700 "heart")) (hearts 650) - , bench "5000-7000:6500 Naive" - $ nf (match (boundedNaive 5000 7000 "heart")) (hearts 6500) - , bench "5000-7000:6500" - $ nf (match (bounded 5000 7000 "heart")) (hearts 6500) - ] - ] - ] +{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Data.Text as Text+import Criterion.Main+import Turtle++boundedNaive :: Int -> Int -> Pattern a -> Pattern [a]+boundedNaive m n p = do+ x <- choice (map pure [m..n])+ count x p++main :: IO ()+main = defaultMain+ [ bgroup "Pattern"+ [ let cats = Text.replicate 1000 "cat"+ furniture = Text.replicate 5 " "+ in bgroup "Cat Lady's House"+ [ bench "Basic"+ $ nf (match (many "cat")) cats+ , bench "Letters"+ $ nf (match (many (mconcat ["c", "a", "t"]))) cats+ , bench "Spaces"+ $ nf (match (many "cat" <* spaces)) (cats <> furniture)+ , bench "Prefix"+ $ nf (match (prefix (many "cat"))) (cats <> furniture)+ ]+ , let hearts n = Text.replicate n "heart"+ in bgroup "Love Knows No Bounds"+ [ bench "500-700:650 Naive"+ $ nf (match (boundedNaive 500 700 "heart")) (hearts 650)+ , bench "500-700:650"+ $ nf (match (bounded 500 700 "heart")) (hearts 650)+ , bench "5000-7000:6500 Naive"+ $ nf (match (boundedNaive 5000 7000 "heart")) (hearts 6500)+ , bench "5000-7000:6500"+ $ nf (match (bounded 5000 7000 "heart")) (hearts 6500)+ ]+ ]+ ]
src/Turtle.hs view
@@ -1,161 +1,161 @@-{-# LANGUAGE CPP #-} -{-# OPTIONS_GHC -fno-warn-name-shadowing #-} - --- | See "Turtle.Tutorial" to learn how to use this library or "Turtle.Prelude" --- for a quick-start guide. --- --- Here is the recommended way to import this library: --- --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > import Prelude hiding (FilePath) --- --- This module re-exports the rest of the library and also re-exports useful --- modules from @base@: --- --- "Turtle.Format" provides type-safe string formatting --- --- "Turtle.Pattern" provides `Pattern`s, which are like more powerful regular --- expressions --- --- "Turtle.Shell" provides a `Shell` abstraction for building streaming, --- exception-safe pipelines --- --- "Turtle.Prelude" provides a library of Unix-like utilities to get you --- started with basic shell-like programming within Haskell --- --- "Control.Applicative" provides two classes: --- --- * `Applicative`, which works with `Fold`, `Pattern`, `Managed`, and `Shell` --- --- * `Alternative`, which works with `Pattern` and `Shell` --- --- "Control.Monad" provides two classes: --- --- * `Monad`, which works with `Pattern`, `Managed` and `Shell` --- --- * `MonadPlus`, which works with `Pattern` and `Shell` --- --- "Control.Monad.IO.Class" provides one class: --- --- * `MonadIO`, which works with `Managed` and `Shell` --- --- "Data.Monoid" provides one class: --- --- * `Monoid`, which works with `Fold`, `Pattern`, `Managed`, and `Shell` --- --- "Control.Monad.Managed.Safe" provides `Managed` resources --- --- "Filesystem.Path.CurrentOS" provides `FilePath`-manipulation utilities --- --- Additionally, you might also want to import the following modules qualified: --- --- * "Options.Applicative" from @optparse-applicative@ for command-line option --- parsing --- --- * "Control.Foldl" (for predefined folds) --- --- * "Control.Foldl.Text" (for `Text`-specific folds) --- --- * "Data.Text" (for `Text`-manipulation utilities) --- --- * "Data.Text.IO" (for reading and writing `Text`) --- --- * "Filesystem.Path.CurrentOS" (for the remaining `FilePath` utilities) - -module Turtle ( - -- * Modules - module Turtle.Format - , module Turtle.Pattern - , module Turtle.Options - , module Turtle.Shell - , module Turtle.Prelude - , module Control.Applicative - , module Control.Monad - , module Control.Monad.IO.Class - , module Data.Monoid - , module Control.Monad.Managed.Safe - , module Filesystem.Path.CurrentOS - , Fold(..) - , FoldM(..) - , Text - , UTCTime - , NominalDiffTime - , Handle - , ExitCode(..) - , IsString(..) - , (&) - ) where - -import Turtle.Format -import Turtle.Pattern -import Turtle.Options -import Turtle.Shell -import Turtle.Prelude -import Control.Applicative - ( Applicative(..) - , Alternative(..) - , (<$>) - , liftA2 - , optional - ) -import Control.Monad - ( MonadPlus(..) - , forever - , void - , (>=>) - , (<=<) - , join - , msum - , mfilter - , replicateM_ - , guard - , when - , unless - ) -import Control.Monad.IO.Class (MonadIO(..)) -import Data.Monoid (Monoid(..), (<>)) -import Data.String (IsString(..)) -import Filesystem.Path.CurrentOS - ( FilePath - , root - , directory - , parent - , filename - , dirname - , basename - , absolute - , relative - , (</>) - , commonPrefix - , stripPrefix - , collapse - , splitDirectories - , extension - , hasExtension - , (<.>) - , dropExtension - , splitExtension - , toText - , fromText - ) -import Control.Monad.Managed.Safe (Managed, managed, runManaged) -import Control.Foldl (Fold(..), FoldM(..)) -import Data.Text (Text) -import Data.Time (NominalDiffTime, UTCTime) -import System.IO (Handle) -import System.Exit (ExitCode(..)) -import Prelude hiding (FilePath) - -#if MIN_VERSION_base(4,8,0) -import Data.Function ((&)) -#else -infixl 1 & - --- | '&' is a reverse application operator. This provides notational --- convenience. Its precedence is one higher than that of the forward --- application operator '$', which allows '&' to be nested in '$'. -(&) :: a -> (a -> b) -> b -x & f = f x -#endif +{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++-- | See "Turtle.Tutorial" to learn how to use this library or "Turtle.Prelude"+-- for a quick-start guide.+--+-- Here is the recommended way to import this library:+--+-- > {-# LANGUAGE OverloadedStrings #-}+-- >+-- > import Turtle+-- > import Prelude hiding (FilePath)+--+-- This module re-exports the rest of the library and also re-exports useful+-- modules from @base@:+--+-- "Turtle.Format" provides type-safe string formatting+--+-- "Turtle.Pattern" provides `Pattern`s, which are like more powerful regular+-- expressions+--+-- "Turtle.Shell" provides a `Shell` abstraction for building streaming,+-- exception-safe pipelines+--+-- "Turtle.Prelude" provides a library of Unix-like utilities to get you+-- started with basic shell-like programming within Haskell+--+-- "Control.Applicative" provides two classes:+--+-- * `Applicative`, which works with `Fold`, `Pattern`, `Managed`, and `Shell`+--+-- * `Alternative`, which works with `Pattern` and `Shell`+--+-- "Control.Monad" provides two classes:+--+-- * `Monad`, which works with `Pattern`, `Managed` and `Shell`+--+-- * `MonadPlus`, which works with `Pattern` and `Shell`+--+-- "Control.Monad.IO.Class" provides one class:+--+-- * `MonadIO`, which works with `Managed` and `Shell`+--+-- "Data.Monoid" provides one class:+--+-- * `Monoid`, which works with `Fold`, `Pattern`, `Managed`, and `Shell`+--+-- "Control.Monad.Managed.Safe" provides `Managed` resources+--+-- "Filesystem.Path.CurrentOS" provides `FilePath`-manipulation utilities+--+-- Additionally, you might also want to import the following modules qualified:+--+-- * "Options.Applicative" from @optparse-applicative@ for command-line option+-- parsing+--+-- * "Control.Foldl" (for predefined folds)+--+-- * "Control.Foldl.Text" (for `Text`-specific folds)+--+-- * "Data.Text" (for `Text`-manipulation utilities)+--+-- * "Data.Text.IO" (for reading and writing `Text`)+--+-- * "Filesystem.Path.CurrentOS" (for the remaining `FilePath` utilities)++module Turtle (+ -- * Modules+ module Turtle.Format+ , module Turtle.Pattern+ , module Turtle.Options+ , module Turtle.Shell+ , module Turtle.Prelude+ , module Control.Applicative+ , module Control.Monad+ , module Control.Monad.IO.Class+ , module Data.Monoid+ , module Control.Monad.Managed.Safe+ , module Filesystem.Path.CurrentOS+ , Fold(..)+ , FoldM(..)+ , Text+ , UTCTime+ , NominalDiffTime+ , Handle+ , ExitCode(..)+ , IsString(..)+ , (&)+ ) where++import Turtle.Format+import Turtle.Pattern+import Turtle.Options+import Turtle.Shell+import Turtle.Prelude+import Control.Applicative+ ( Applicative(..)+ , Alternative(..)+ , (<$>)+ , liftA2+ , optional+ )+import Control.Monad+ ( MonadPlus(..)+ , forever+ , void+ , (>=>)+ , (<=<)+ , join+ , msum+ , mfilter+ , replicateM_+ , guard+ , when+ , unless+ )+import Control.Monad.IO.Class (MonadIO(..))+import Data.Monoid (Monoid(..), (<>))+import Data.String (IsString(..))+import Filesystem.Path.CurrentOS+ ( FilePath+ , root+ , directory+ , parent+ , filename+ , dirname+ , basename+ , absolute+ , relative+ , (</>)+ , commonPrefix+ , stripPrefix+ , collapse+ , splitDirectories+ , extension+ , hasExtension+ , (<.>)+ , dropExtension+ , splitExtension+ , toText+ , fromText+ )+import Control.Monad.Managed.Safe (Managed, managed, runManaged)+import Control.Foldl (Fold(..), FoldM(..))+import Data.Text (Text)+import Data.Time (NominalDiffTime, UTCTime)+import System.IO (Handle)+import System.Exit (ExitCode(..))+import Prelude hiding (FilePath)++#if MIN_VERSION_base(4,8,0)+import Data.Function ((&))+#else+infixl 1 &++-- | '&' is a reverse application operator. This provides notational+-- convenience. Its precedence is one higher than that of the forward+-- application operator '$', which allows '&' to be nested in '$'.+(&) :: a -> (a -> b) -> b+x & f = f x+#endif
src/Turtle/Format.hs view
@@ -1,195 +1,195 @@-{-# LANGUAGE OverloadedStrings #-} - -{-| Minimalist implementation of type-safe formatted strings, borrowing heavily - from the implementation of the @formatting@ package. - - Example use of this module: - ->>> :set -XOverloadedStrings ->>> import Turtle.Format ->>> format ("This is a "%s%" string that takes "%d%" arguments") "format" 2 -"This is a format string that takes 2 arguments" - - A `Format` string that takes no arguments has this type: - -> "I take 0 arguments" :: Format r r -> -> format "I take 0 arguments" :: Text - ->>> format "I take 0 arguments" -"I take 0 arguments" - - A `Format` string that takes one argument has this type: - -> "I take "%d%" arguments" :: Format r (Int -> r) -> -> format ("I take "%d%" argument") :: Int -> Text - ->>> format ("I take "%d%" argument") 1 -"I take 1 argument" - - A `Format` string that takes two arguments has this type: - -> "I "%s%" "%d%" arguments" :: Format r (Text -> Int -> r) -> -> format ("I "%s%" "%d%" arguments") :: Text -> Int -> Text - ->>> format ("I "%s%" "%d%" arguments") "take" 2 -"I take 2 arguments" --} - -{-# LANGUAGE TypeFamilies #-} - -module Turtle.Format ( - -- * Format - Format - , (%) - , format - , makeFormat - - -- * Parameters - , w - , d - , u - , o - , x - , f - , e - , g - , s - , fp - - -- * Utilities - , repr - ) where - -import Control.Category (Category(..)) -import Data.Monoid ((<>)) -import Data.String (IsString(..)) -import Data.Text (Text, pack) -import Data.Word (Word) -import Filesystem.Path.CurrentOS (FilePath, toText) -import Numeric (showEFloat, showFFloat, showGFloat, showHex, showOct) -import Prelude hiding ((.), id, FilePath) - --- | A `Format` string -newtype Format a b = Format { (>>-) :: (Text -> a) -> b } - -instance Category Format where - id = Format (\return_ -> return_ "") - - fmt1 . fmt2 = Format (\return_ -> - fmt1 >>- \str1 -> - fmt2 >>- \str2 -> - return_ (str1 <> str2) ) - --- | Concatenate two `Format` strings -(%) :: Format b c -> Format a b -> Format a c -(%) = (.) - -instance (a ~ b) => IsString (Format a b) where - fromString str = Format (\return_ -> return_ (pack str)) - -{-| Convert a `Format` string to a print function that takes zero or more typed - arguments and returns a `Text` string --} -format :: Format Text r -> r -format fmt = fmt >>- id - --- | Create your own format specifier -makeFormat :: (a -> Text) -> Format r (a -> r) -makeFormat k = Format (\return_ -> \a -> return_ (k a)) - -{-| `Format` any `Show`able value - ->>> format w True -"True" --} -w :: Show a => Format r (a -> r) -w = makeFormat (pack . show) - -{-| `Format` an `Int` value as a signed decimal - ->>> format d 25 -"25" ->>> format d (-25) -"-25" --} -d :: Format r (Int -> r) -d = w - -{-| `Format` a `Word` value as an unsigned decimal - ->>> format u 25 -"25" --} -u :: Format r (Word -> r) -u = w - -{-| `Format` a `Word` value as an unsigned octal number - ->>> format o 25 -"31" --} -o :: Format r (Word -> r) -o = makeFormat (\n -> pack (showOct n "")) - -{-| `Format` a `Word` value as an unsigned hexadecimal number (without a - leading \"0x\") - ->>> format x 25 -"19" --} -x :: Format r (Word -> r) -x = makeFormat (\n -> pack (showHex n "")) - -{-| `Format` a `Double` using decimal notation with 6 digits of precision - ->>> format f 25.1 -"25.100000" --} -f :: Format r (Double -> r) -f = makeFormat (\n -> pack (showFFloat (Just 6) n "")) - -{-| `Format` a `Double` using scientific notation with 6 digits of precision - ->>> format e 25.1 -"2.510000e1" --} -e :: Format r (Double -> r) -e = makeFormat (\n -> pack (showEFloat (Just 6) n "")) - -{-| `Format` a `Double` using decimal notation for small exponents and - scientific notation for large exponents - ->>> format g 25.1 -"25.100000" ->>> format g 123456789 -"1.234568e8" ->>> format g 0.00000000001 -"1.000000e-11" --} -g :: Format r (Double -> r) -g = makeFormat (\n -> pack (showGFloat (Just 6) n "")) - -{-| `Format` that inserts `Text` - ->>> format s "ABC" -"ABC" --} -s :: Format r (Text -> r) -s = makeFormat id - --- | `Format` a `Filesystem.Path.CurrentOS.FilePath` into `Text` -fp :: Format r (FilePath -> r) -fp = makeFormat (\fpath -> either id id (toText fpath)) - -{-| Convert a `Show`able value to `Text` - - Short-hand for @(format w)@ - ->>> repr (1,2) -"(1,2)" --} -repr :: Show a => a -> Text -repr = format w +{-# LANGUAGE OverloadedStrings #-}++{-| Minimalist implementation of type-safe formatted strings, borrowing heavily+ from the implementation of the @formatting@ package.++ Example use of this module:++>>> :set -XOverloadedStrings+>>> import Turtle.Format+>>> format ("This is a "%s%" string that takes "%d%" arguments") "format" 2+"This is a format string that takes 2 arguments"++ A `Format` string that takes no arguments has this type:++> "I take 0 arguments" :: Format r r+>+> format "I take 0 arguments" :: Text++>>> format "I take 0 arguments"+"I take 0 arguments"++ A `Format` string that takes one argument has this type:++> "I take "%d%" arguments" :: Format r (Int -> r)+>+> format ("I take "%d%" argument") :: Int -> Text++>>> format ("I take "%d%" argument") 1+"I take 1 argument"++ A `Format` string that takes two arguments has this type:++> "I "%s%" "%d%" arguments" :: Format r (Text -> Int -> r)+>+> format ("I "%s%" "%d%" arguments") :: Text -> Int -> Text++>>> format ("I "%s%" "%d%" arguments") "take" 2+"I take 2 arguments"+-}++{-# LANGUAGE TypeFamilies #-}++module Turtle.Format (+ -- * Format+ Format+ , (%)+ , format+ , makeFormat++ -- * Parameters+ , w+ , d+ , u+ , o+ , x+ , f+ , e+ , g+ , s+ , fp++ -- * Utilities+ , repr+ ) where++import Control.Category (Category(..))+import Data.Monoid ((<>))+import Data.String (IsString(..))+import Data.Text (Text, pack)+import Data.Word (Word)+import Filesystem.Path.CurrentOS (FilePath, toText)+import Numeric (showEFloat, showFFloat, showGFloat, showHex, showOct)+import Prelude hiding ((.), id, FilePath)++-- | A `Format` string+newtype Format a b = Format { (>>-) :: (Text -> a) -> b }++instance Category Format where+ id = Format (\return_ -> return_ "")++ fmt1 . fmt2 = Format (\return_ ->+ fmt1 >>- \str1 ->+ fmt2 >>- \str2 ->+ return_ (str1 <> str2) )++-- | Concatenate two `Format` strings+(%) :: Format b c -> Format a b -> Format a c+(%) = (.)++instance (a ~ b) => IsString (Format a b) where+ fromString str = Format (\return_ -> return_ (pack str))++{-| Convert a `Format` string to a print function that takes zero or more typed+ arguments and returns a `Text` string+-}+format :: Format Text r -> r+format fmt = fmt >>- id++-- | Create your own format specifier+makeFormat :: (a -> Text) -> Format r (a -> r)+makeFormat k = Format (\return_ -> \a -> return_ (k a))++{-| `Format` any `Show`able value++>>> format w True+"True"+-}+w :: Show a => Format r (a -> r)+w = makeFormat (pack . show)++{-| `Format` an `Int` value as a signed decimal++>>> format d 25+"25"+>>> format d (-25)+"-25"+-}+d :: Format r (Int -> r)+d = w++{-| `Format` a `Word` value as an unsigned decimal++>>> format u 25+"25"+-}+u :: Format r (Word -> r)+u = w++{-| `Format` a `Word` value as an unsigned octal number++>>> format o 25+"31"+-}+o :: Format r (Word -> r)+o = makeFormat (\n -> pack (showOct n ""))++{-| `Format` a `Word` value as an unsigned hexadecimal number (without a+ leading \"0x\")++>>> format x 25+"19"+-}+x :: Format r (Word -> r)+x = makeFormat (\n -> pack (showHex n ""))++{-| `Format` a `Double` using decimal notation with 6 digits of precision++>>> format f 25.1+"25.100000"+-}+f :: Format r (Double -> r)+f = makeFormat (\n -> pack (showFFloat (Just 6) n ""))++{-| `Format` a `Double` using scientific notation with 6 digits of precision++>>> format e 25.1+"2.510000e1"+-}+e :: Format r (Double -> r)+e = makeFormat (\n -> pack (showEFloat (Just 6) n ""))++{-| `Format` a `Double` using decimal notation for small exponents and+ scientific notation for large exponents++>>> format g 25.1+"25.100000"+>>> format g 123456789+"1.234568e8"+>>> format g 0.00000000001+"1.000000e-11"+-}+g :: Format r (Double -> r)+g = makeFormat (\n -> pack (showGFloat (Just 6) n ""))++{-| `Format` that inserts `Text`++>>> format s "ABC"+"ABC"+-}+s :: Format r (Text -> r)+s = makeFormat id++-- | `Format` a `Filesystem.Path.CurrentOS.FilePath` into `Text`+fp :: Format r (FilePath -> r)+fp = makeFormat (\fpath -> either id id (toText fpath))++{-| Convert a `Show`able value to `Text`++ Short-hand for @(format w)@++>>> repr (1,2)+"(1,2)"+-}+repr :: Show a => a -> Text+repr = format w
src/Turtle/Options.hs view
@@ -1,208 +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 +{-# 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
@@ -1,675 +1,675 @@-{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TypeFamilies #-} -{-# OPTIONS_GHC -fno-warn-missing-methods #-} - -{-| Use this module to either: - - * match `Text` with light-weight backtracking patterns, or: - - * parse structured values from `Text`. - - Example usage: - ->>> :set -XOverloadedStrings ->>> match ("can" <|> "cat") "cat" -["cat"] ->>> match ("can" <|> "cat") "dog" -[] ->>> match (decimal `sepBy` ",") "1,2,3" -[[1,2,3]] - - This pattern has unlimited backtracking, and will return as many solutions - as possible: - ->>> match (prefix (star anyChar)) "123" -["123","12","1",""] - - Use @do@ notation to structure more complex patterns: - ->>> :{ -let bit = ("0" *> pure False) <|> ("1" *> pure True) :: Pattern Bool; - portableBitMap = do - { "P1" - ; width <- spaces1 *> decimal - ; height <- spaces1 *> decimal - ; count width (count height (spaces1 *> bit)) - }; -in match (prefix portableBitMap) "P1\n2 2\n0 0\n1 0\n" -:} -[[[False,False],[True,False]]] - --} - -module Turtle.Pattern ( - -- * Pattern - Pattern - , match - - -- * Primitive patterns - , anyChar - , eof - - -- * Character patterns - , dot - , satisfy - , char - , notChar - , text - , asciiCI - , oneOf - , noneOf - , space - , spaces - , spaces1 - , tab - , newline - , crlf - , upper - , lower - , alphaNum - , letter - , digit - , hexDigit - , octDigit - - -- * Numbers - , decimal - , signed - - -- * Combinators - , prefix - , suffix - , has - , invert - , once - , star - , plus - , selfless - , choice - , count - , upperBounded - , bounded - , option - , between - , skip - , within - , fixed - , sepBy - , sepBy1 - - -- * High-efficiency primitives - , chars - , chars1 - ) where - -import Control.Applicative -import Control.Monad -import Control.Monad.Trans.Class (lift) -import Control.Monad.Trans.State -import Data.Char -import Data.List (foldl') -import Data.Monoid (Monoid(..), (<>)) -import Data.String (IsString(..)) -import Data.Text (Text) -import qualified Data.Text as Text - --- | A fully backtracking pattern that parses an @\'a\'@ from some `Text` -newtype Pattern a = Pattern { runPattern :: StateT Text [] a } - deriving (Functor, Applicative, Monad, Alternative, MonadPlus) - -instance Monoid a => Monoid (Pattern a) where - mempty = pure mempty - mappend = liftA2 mappend - --- | Pattern forms a semiring, this is the closest approximation -instance Monoid a => Num (Pattern a) where - fromInteger n = Pattern (lift (replicate (fromInteger n) mempty)) - (+) = (<|>) - (*) = (<>) - -instance (a ~ Text) => IsString (Pattern a) where - fromString str = text (Text.pack str) - -{-| Match a `Pattern` against a `Text` input, returning all possible solutions - - The `Pattern` must match the entire `Text` --} -match :: Pattern a -> Text -> [a] -match p = evalStateT (runPattern (p <* eof)) - -{-| Match any character - ->>> match anyChar "1" -"1" ->>> match anyChar "" -"" --} -anyChar :: Pattern Char -anyChar = Pattern (do - Just (c, cs) <- fmap Text.uncons get - put cs - return c ) - -{-| Matches the end of input - ->>> match eof "1" -[] ->>> match eof "" -[()] --} -eof :: Pattern () -eof = Pattern (do - True <- fmap Text.null get - return () ) - --- | Synonym for `anyChar` -dot :: Pattern Char -dot = anyChar - -{-| Match any character that satisfies the given predicate - ->>> match (satisfy (== '1')) "1" -"1" ->>> match (satisfy (== '2')) "1" -"" --} -satisfy :: (Char -> Bool) -> Pattern Char -satisfy predicate = do - c <- anyChar - guard (predicate c) - return c - -{-| Match a specific character - ->>> match (char '1') "1" -"1" ->>> match (char '2') "1" -"" --} -char :: Char -> Pattern Char -char c = satisfy (== c) - -{-| Match any character except the given one - ->>> match (notChar '2') "1" -"1" ->>> match (notChar '1') "1" -"" --} -notChar :: Char -> Pattern Char -notChar c = satisfy (/= c) - -{-| Match a specific string - ->>> match (text "123") "123" -["123"] - - You can also omit the `text` function if you enable the @OverloadedStrings@ - extension: - ->>> match "123" "123" -["123"] --} -text :: Text -> Pattern Text -text before' = Pattern (do - txt <- get - let (before, after) = Text.splitAt (Text.length before') txt - guard (before == before') - put after - return before) - -{-| Match a specific string in a case-insensitive way - - This only handles ASCII strings - ->>> match (asciiCI "abc") "ABC" -["ABC"] --} -asciiCI :: Text -> Pattern Text -asciiCI before' = Pattern (do - txt <- get - let (before, after) = Text.splitAt (Text.length before') txt - guard (lowerChars before == lowerChars before') - put after - return before ) - where - lowerChars = Text.map lowerChar - lowerChar c | 'A' <= c && c <= 'Z' = chr (ord c + ord 'a' - ord 'A') - | otherwise = c - -{-| Match any one of the given characters - ->>> match (oneOf "1a") "1" -"1" ->>> match (oneOf "2a") "1" -"" --} -oneOf :: [Char] -> Pattern Char -oneOf cs = satisfy (`elem` cs) - -{-| Match anything other than the given characters - ->>> match (noneOf "2a") "1" -"1" ->>> match (noneOf "1a") "1" -"" --} -noneOf :: [Char] -> Pattern Char -noneOf cs = satisfy (`notElem` cs) - -{-| Match a whitespace character - ->>> match space " " -" " ->>> match space "1" -"" --} -space :: Pattern Char -space = satisfy isSpace - -{-| Match zero or more whitespace characters - ->>> match spaces " " -[" "] ->>> match spaces "" -[""] --} -spaces :: Pattern Text -spaces = star space - -{-| Match one or more whitespace characters - ->>> match spaces1 " " -[" "] ->>> match spaces1 "" -[] --} -spaces1 :: Pattern Text -spaces1 = plus space - -{-| Match the tab character (@\'\t\'@) - ->>> match tab "\t" -"\t" ->>> match tab " " -"" --} -tab :: Pattern Char -tab = char '\t' - -{-| Match the newline character (@\'\n\'@) - ->>> match newline "\n" -"\n" ->>> match newline " " -"" --} -newline :: Pattern Char -newline = char '\n' - -{-| Matches a carriage return (@\'\r\'@) followed by a newline (@\'\n\'@) - ->>> match crlf "\r\n" -["\r\n"] ->>> match crlf "\n\r" -[] --} -crlf :: Pattern Text -crlf = text "\r\n" - -{-| Match an uppercase letter - ->>> match upper "A" -"A" ->>> match upper "a" -"" --} -upper :: Pattern Char -upper = satisfy isUpper - -{-| Match a lowercase letter - ->>> match lower "a" -"a" ->>> match lower "A" -"" --} -lower :: Pattern Char -lower = satisfy isLower - -{-| Match a letter or digit - ->>> match alphaNum "1" -"1" ->>> match alphaNum "a" -"a" ->>> match alphaNum "A" -"A" ->>> match alphaNum "." -"" --} -alphaNum :: Pattern Char -alphaNum = satisfy isAlphaNum - -{-| Match a letter - ->>> match letter "A" -"A" ->>> match letter "a" -"a" ->>> match letter "1" -"" --} -letter :: Pattern Char -letter = satisfy isLetter - -{-| Match a digit - ->>> match digit "1" -"1" ->>> match digit "a" -"" --} -digit :: Pattern Char -digit = satisfy isDigit - -{-| Match a hexadecimal digit - ->>> match hexDigit "1" -"1" ->>> match hexDigit "A" -"A" ->>> match hexDigit "a" -"a" ->>> match hexDigit "g" -"" --} -hexDigit :: Pattern Char -hexDigit = satisfy isHexDigit - -{-| Match an octal digit - ->>> match octDigit "1" -"1" ->>> match octDigit "9" -"" --} -octDigit :: Pattern Char -octDigit = satisfy isOctDigit - -{-| Match an unsigned decimal number - ->>> match decimal "123" -[123] ->>> match decimal "-123" -[] --} -decimal :: Num n => Pattern n -decimal = do - ds <- some digit - return (foldl' step 0 ds) - where - step n d = n * 10 + fromIntegral (ord d - ord '0') - -{-| Transform a numeric parser to accept an optional leading @\'+\'@ or @\'-\'@ - sign - ->>> match (signed decimal) "+123" -[123] ->>> match (signed decimal) "-123" -[-123] ->>> match (signed decimal) "123" -[123] --} -signed :: Num a => Pattern a -> Pattern a -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` - ->>> match (once (char '1')) "1" -["1"] ->>> match (once (char '1')) "" -[] --} -once :: Pattern Char -> Pattern Text -once p = fmap Text.singleton p - -{-| Use this to match the prefix of a string - ->>> match "A" "ABC" -[] ->>> match (prefix "A") "ABC" -["A"] --} -prefix :: Pattern a -> Pattern a -prefix p = p <* chars - -{-| Use this to match the suffix of a string - ->>> match "C" "ABC" -[] ->>> match (suffix "C") "ABC" -["C"] --} -suffix :: Pattern a -> Pattern a -suffix p = chars *> p - -{-| Use this to match the interior of a string - ->>> match "B" "ABC" -[] ->>> match (has "B") "ABC" -["B"] --} -has :: Pattern a -> Pattern a -has p = chars *> p <* chars - -{-| Parse 0 or more occurrences of the given character - ->>> match (star anyChar) "123" -["123"] ->>> match (star anyChar) "" -[""] - - See also: `chars` --} -star :: Pattern Char -> Pattern Text -star p = fmap Text.pack (many p) - -{-| Parse 1 or more occurrences of the given character - ->>> match (plus digit) "123" -["123"] ->>> match (plus digit) "" -[] - - See also: `chars1` --} -plus :: Pattern Char -> Pattern Text -plus p = fmap Text.pack (some p) - -{-| Patterns that match multiple times are greedy by default, meaning that they - try to match as many times as possible. The `selfless` combinator makes a - pattern match as few times as possible - - This only changes the order in which solutions are returned, by prioritizing - less greedy solutions - ->>> match (prefix (selfless (some anyChar))) "123" -["1","12","123"] ->>> match (prefix (some anyChar) ) "123" -["123","12","1"] --} -selfless :: Pattern a -> Pattern a -selfless p = Pattern (StateT (\s -> reverse (runStateT (runPattern p) s))) - -{-| Apply the patterns in the list in order, until one of them succeeds - ->>> match (choice ["cat", "dog", "egg"]) "egg" -["egg"] ->>> match (choice ["cat", "dog", "egg"]) "cat" -["cat"] ->>> match (choice ["cat", "dog", "egg"]) "fan" -[] --} -choice :: [Pattern a] -> Pattern a -choice = msum - -{-| Apply the given pattern a fixed number of times, collecting the results - ->>> match (count 3 anyChar) "123" -["123"] ->>> match (count 4 anyChar) "123" -[] --} -count :: Int -> Pattern a -> Pattern [a] -count = replicateM - -{-| Apply the given pattern 0 or more times, up to a given bound, - collecting the results - ->>> match (upperBounded 5 dot) "123" -["123"] ->>> match (upperBounded 2 dot) "123" -[] ->>> match ((,) <$> upperBounded 2 dot <*> chars) "123" -[("12","3"),("1","23")] --} - -upperBounded :: Int -> Pattern a -> Pattern [a] -upperBounded n p - | n <= 0 = mempty - | n == 1 = fmap pure p - | otherwise = (:) <$> p <*> option (upperBounded (n - 1) p) - -{-| Apply the given pattern a number of times restricted by given - lower and upper bounds, collecting the results - ->>> match (bounded 2 5 "cat") "catcatcat" -[["cat","cat","cat"]] ->>> match (bounded 2 5 "cat") "cat" -[] ->>> match (bounded 2 5 "cat") "catcatcatcatcatcat" -[] - -`bounded` could be implemented naively as follows: - -> bounded m n p = do -> x <- choice (map pure [m..n]) -> count x p - --} - -bounded :: Int -> Int -> Pattern a -> Pattern [a] -bounded m n p - | m == n = count m p - | m < n = (++) <$> count m p <*> option (upperBounded (n - m) p) - | otherwise = mzero - -{-| Transform a parser to a succeed with an empty value instead of failing - - See also: `optional` - ->>> match (option "1" <> "2") "12" -["12"] ->>> match (option "1" <> "2") "2" -["2"] --} -option :: Monoid a => Pattern a -> Pattern a -option p = p <|> mempty - -{-| @(between open close p)@ matches @\'p\'@ in between @\'open\'@ and - @\'close\'@ - ->>> match (between (char '(') (char ')') (star anyChar)) "(123)" -["123"] ->>> match (between (char '(') (char ')') (star anyChar)) "(123" -[] --} -between :: Pattern a -> Pattern b -> Pattern c -> Pattern c -between open close p = open *> p <* close - -{-| Discard the pattern's result - ->>> match (skip anyChar) "1" -[()] ->>> match (skip anyChar) "" -[] --} -skip :: Pattern a -> Pattern () -skip = void - -{-| Restrict the pattern to consume no more than the given number of characters - ->>> match (within 2 decimal) "12" -[12] ->>> match (within 2 decimal) "1" -[1] ->>> match (within 2 decimal) "123" -[] --} -within :: Int -> Pattern a -> Pattern a -within n p = Pattern (do - txt <- get - let (before, after) = Text.splitAt n txt - put before - a <- runPattern p - modify (<> after) - return a ) - -{-| Require the pattern to consume exactly the given number of characters - ->>> match (fixed 2 decimal) "12" -[12] ->>> match (fixed 2 decimal) "1" -[] --} -fixed :: Int -> Pattern a -> Pattern a -fixed n p = do - txt <- Pattern get - guard (Text.length txt >= n) - within n (p <* eof) - -{-| @p `sepBy` sep@ matches zero or more occurrences of @p@ separated by @sep@ - ->>> match (decimal `sepBy` char ',') "1,2,3" -[[1,2,3]] ->>> match (decimal `sepBy` char ',') "" -[[]] --} -sepBy :: Pattern a -> Pattern b -> Pattern [a] -p `sepBy` sep = (p `sepBy1` sep) <|> pure [] - -{-| @p `sepBy1` sep@ matches one or more occurrences of @p@ separated by @sep@ - ->>> match (decimal `sepBy1` ",") "1,2,3" -[[1,2,3]] ->>> match (decimal `sepBy1` ",") "" -[] --} -sepBy1 :: Pattern a -> Pattern b -> Pattern [a] -p `sepBy1` sep = (:) <$> p <*> many (sep *> p) - --- | Like @star dot@ or @star anyChar@, except more efficient -chars :: Pattern Text -chars = Pattern (StateT (\txt -> - reverse (zip (Text.inits txt) (Text.tails txt)) )) - --- | Like @plus dot@ or @plus anyChar@, except more efficient -chars1 :: Pattern Text -chars1 = Text.cons <$> dot <*> chars +{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -fno-warn-missing-methods #-}++{-| Use this module to either:++ * match `Text` with light-weight backtracking patterns, or:++ * parse structured values from `Text`.++ Example usage:++>>> :set -XOverloadedStrings+>>> match ("can" <|> "cat") "cat"+["cat"]+>>> match ("can" <|> "cat") "dog"+[]+>>> match (decimal `sepBy` ",") "1,2,3"+[[1,2,3]]++ This pattern has unlimited backtracking, and will return as many solutions+ as possible:++>>> match (prefix (star anyChar)) "123"+["123","12","1",""]++ Use @do@ notation to structure more complex patterns:++>>> :{+let bit = ("0" *> pure False) <|> ("1" *> pure True) :: Pattern Bool;+ portableBitMap = do+ { "P1"+ ; width <- spaces1 *> decimal+ ; height <- spaces1 *> decimal+ ; count width (count height (spaces1 *> bit))+ };+in match (prefix portableBitMap) "P1\n2 2\n0 0\n1 0\n"+:}+[[[False,False],[True,False]]]++-}++module Turtle.Pattern (+ -- * Pattern+ Pattern+ , match++ -- * Primitive patterns+ , anyChar+ , eof++ -- * Character patterns+ , dot+ , satisfy+ , char+ , notChar+ , text+ , asciiCI+ , oneOf+ , noneOf+ , space+ , spaces+ , spaces1+ , tab+ , newline+ , crlf+ , upper+ , lower+ , alphaNum+ , letter+ , digit+ , hexDigit+ , octDigit++ -- * Numbers+ , decimal+ , signed++ -- * Combinators+ , prefix+ , suffix+ , has+ , invert+ , once+ , star+ , plus+ , selfless+ , choice+ , count+ , upperBounded+ , bounded+ , option+ , between+ , skip+ , within+ , fixed+ , sepBy+ , sepBy1++ -- * High-efficiency primitives+ , chars+ , chars1+ ) where++import Control.Applicative+import Control.Monad+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.State+import Data.Char+import Data.List (foldl')+import Data.Monoid (Monoid(..), (<>))+import Data.String (IsString(..))+import Data.Text (Text)+import qualified Data.Text as Text++-- | A fully backtracking pattern that parses an @\'a\'@ from some `Text`+newtype Pattern a = Pattern { runPattern :: StateT Text [] a }+ deriving (Functor, Applicative, Monad, Alternative, MonadPlus)++instance Monoid a => Monoid (Pattern a) where+ mempty = pure mempty+ mappend = liftA2 mappend++-- | Pattern forms a semiring, this is the closest approximation+instance Monoid a => Num (Pattern a) where+ fromInteger n = Pattern (lift (replicate (fromInteger n) mempty))+ (+) = (<|>)+ (*) = (<>)++instance (a ~ Text) => IsString (Pattern a) where+ fromString str = text (Text.pack str)++{-| Match a `Pattern` against a `Text` input, returning all possible solutions++ The `Pattern` must match the entire `Text`+-}+match :: Pattern a -> Text -> [a]+match p = evalStateT (runPattern (p <* eof))++{-| Match any character++>>> match anyChar "1"+"1"+>>> match anyChar ""+""+-}+anyChar :: Pattern Char+anyChar = Pattern (do+ Just (c, cs) <- fmap Text.uncons get+ put cs+ return c )++{-| Matches the end of input++>>> match eof "1"+[]+>>> match eof ""+[()]+-}+eof :: Pattern ()+eof = Pattern (do+ True <- fmap Text.null get+ return () )++-- | Synonym for `anyChar`+dot :: Pattern Char+dot = anyChar++{-| Match any character that satisfies the given predicate++>>> match (satisfy (== '1')) "1"+"1"+>>> match (satisfy (== '2')) "1"+""+-}+satisfy :: (Char -> Bool) -> Pattern Char+satisfy predicate = do+ c <- anyChar+ guard (predicate c)+ return c++{-| Match a specific character++>>> match (char '1') "1"+"1"+>>> match (char '2') "1"+""+-}+char :: Char -> Pattern Char+char c = satisfy (== c)++{-| Match any character except the given one++>>> match (notChar '2') "1"+"1"+>>> match (notChar '1') "1"+""+-}+notChar :: Char -> Pattern Char+notChar c = satisfy (/= c)++{-| Match a specific string++>>> match (text "123") "123"+["123"]++ You can also omit the `text` function if you enable the @OverloadedStrings@+ extension:++>>> match "123" "123"+["123"]+-}+text :: Text -> Pattern Text+text before' = Pattern (do+ txt <- get+ let (before, after) = Text.splitAt (Text.length before') txt+ guard (before == before')+ put after+ return before)++{-| Match a specific string in a case-insensitive way++ This only handles ASCII strings++>>> match (asciiCI "abc") "ABC"+["ABC"]+-}+asciiCI :: Text -> Pattern Text+asciiCI before' = Pattern (do+ txt <- get+ let (before, after) = Text.splitAt (Text.length before') txt+ guard (lowerChars before == lowerChars before')+ put after+ return before )+ where+ lowerChars = Text.map lowerChar+ lowerChar c | 'A' <= c && c <= 'Z' = chr (ord c + ord 'a' - ord 'A')+ | otherwise = c++{-| Match any one of the given characters++>>> match (oneOf "1a") "1"+"1"+>>> match (oneOf "2a") "1"+""+-}+oneOf :: [Char] -> Pattern Char+oneOf cs = satisfy (`elem` cs)++{-| Match anything other than the given characters++>>> match (noneOf "2a") "1"+"1"+>>> match (noneOf "1a") "1"+""+-}+noneOf :: [Char] -> Pattern Char+noneOf cs = satisfy (`notElem` cs)++{-| Match a whitespace character++>>> match space " "+" "+>>> match space "1"+""+-}+space :: Pattern Char+space = satisfy isSpace++{-| Match zero or more whitespace characters++>>> match spaces " "+[" "]+>>> match spaces ""+[""]+-}+spaces :: Pattern Text+spaces = star space++{-| Match one or more whitespace characters++>>> match spaces1 " "+[" "]+>>> match spaces1 ""+[]+-}+spaces1 :: Pattern Text+spaces1 = plus space++{-| Match the tab character (@\'\t\'@)++>>> match tab "\t"+"\t"+>>> match tab " "+""+-}+tab :: Pattern Char+tab = char '\t'++{-| Match the newline character (@\'\n\'@)++>>> match newline "\n"+"\n"+>>> match newline " "+""+-}+newline :: Pattern Char+newline = char '\n'++{-| Matches a carriage return (@\'\r\'@) followed by a newline (@\'\n\'@)++>>> match crlf "\r\n"+["\r\n"]+>>> match crlf "\n\r"+[]+-}+crlf :: Pattern Text+crlf = text "\r\n"++{-| Match an uppercase letter++>>> match upper "A"+"A"+>>> match upper "a"+""+-}+upper :: Pattern Char+upper = satisfy isUpper++{-| Match a lowercase letter++>>> match lower "a"+"a"+>>> match lower "A"+""+-}+lower :: Pattern Char+lower = satisfy isLower++{-| Match a letter or digit++>>> match alphaNum "1"+"1"+>>> match alphaNum "a"+"a"+>>> match alphaNum "A"+"A"+>>> match alphaNum "."+""+-}+alphaNum :: Pattern Char+alphaNum = satisfy isAlphaNum++{-| Match a letter++>>> match letter "A"+"A"+>>> match letter "a"+"a"+>>> match letter "1"+""+-}+letter :: Pattern Char+letter = satisfy isLetter++{-| Match a digit++>>> match digit "1"+"1"+>>> match digit "a"+""+-}+digit :: Pattern Char+digit = satisfy isDigit++{-| Match a hexadecimal digit++>>> match hexDigit "1"+"1"+>>> match hexDigit "A"+"A"+>>> match hexDigit "a"+"a"+>>> match hexDigit "g"+""+-}+hexDigit :: Pattern Char+hexDigit = satisfy isHexDigit++{-| Match an octal digit++>>> match octDigit "1"+"1"+>>> match octDigit "9"+""+-}+octDigit :: Pattern Char+octDigit = satisfy isOctDigit++{-| Match an unsigned decimal number++>>> match decimal "123"+[123]+>>> match decimal "-123"+[]+-}+decimal :: Num n => Pattern n+decimal = do+ ds <- some digit+ return (foldl' step 0 ds)+ where+ step n d = n * 10 + fromIntegral (ord d - ord '0')++{-| Transform a numeric parser to accept an optional leading @\'+\'@ or @\'-\'@+ sign++>>> match (signed decimal) "+123"+[123]+>>> match (signed decimal) "-123"+[-123]+>>> match (signed decimal) "123"+[123]+-}+signed :: Num a => Pattern a -> Pattern a+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`++>>> match (once (char '1')) "1"+["1"]+>>> match (once (char '1')) ""+[]+-}+once :: Pattern Char -> Pattern Text+once p = fmap Text.singleton p++{-| Use this to match the prefix of a string++>>> match "A" "ABC"+[]+>>> match (prefix "A") "ABC"+["A"]+-}+prefix :: Pattern a -> Pattern a+prefix p = p <* chars++{-| Use this to match the suffix of a string++>>> match "C" "ABC"+[]+>>> match (suffix "C") "ABC"+["C"]+-}+suffix :: Pattern a -> Pattern a+suffix p = chars *> p++{-| Use this to match the interior of a string++>>> match "B" "ABC"+[]+>>> match (has "B") "ABC"+["B"]+-}+has :: Pattern a -> Pattern a+has p = chars *> p <* chars++{-| Parse 0 or more occurrences of the given character++>>> match (star anyChar) "123"+["123"]+>>> match (star anyChar) ""+[""]++ See also: `chars`+-}+star :: Pattern Char -> Pattern Text+star p = fmap Text.pack (many p)++{-| Parse 1 or more occurrences of the given character++>>> match (plus digit) "123"+["123"]+>>> match (plus digit) ""+[]++ See also: `chars1`+-}+plus :: Pattern Char -> Pattern Text+plus p = fmap Text.pack (some p)++{-| Patterns that match multiple times are greedy by default, meaning that they+ try to match as many times as possible. The `selfless` combinator makes a+ pattern match as few times as possible++ This only changes the order in which solutions are returned, by prioritizing+ less greedy solutions++>>> match (prefix (selfless (some anyChar))) "123"+["1","12","123"]+>>> match (prefix (some anyChar) ) "123"+["123","12","1"]+-}+selfless :: Pattern a -> Pattern a+selfless p = Pattern (StateT (\s -> reverse (runStateT (runPattern p) s)))++{-| Apply the patterns in the list in order, until one of them succeeds++>>> match (choice ["cat", "dog", "egg"]) "egg"+["egg"]+>>> match (choice ["cat", "dog", "egg"]) "cat"+["cat"]+>>> match (choice ["cat", "dog", "egg"]) "fan"+[]+-}+choice :: [Pattern a] -> Pattern a+choice = msum++{-| Apply the given pattern a fixed number of times, collecting the results++>>> match (count 3 anyChar) "123"+["123"]+>>> match (count 4 anyChar) "123"+[]+-}+count :: Int -> Pattern a -> Pattern [a]+count = replicateM++{-| Apply the given pattern 0 or more times, up to a given bound,+ collecting the results++>>> match (upperBounded 5 dot) "123"+["123"]+>>> match (upperBounded 2 dot) "123"+[]+>>> match ((,) <$> upperBounded 2 dot <*> chars) "123"+[("12","3"),("1","23")]+-}++upperBounded :: Int -> Pattern a -> Pattern [a]+upperBounded n p+ | n <= 0 = mempty+ | n == 1 = fmap pure p+ | otherwise = (:) <$> p <*> option (upperBounded (n - 1) p)++{-| Apply the given pattern a number of times restricted by given+ lower and upper bounds, collecting the results++>>> match (bounded 2 5 "cat") "catcatcat"+[["cat","cat","cat"]]+>>> match (bounded 2 5 "cat") "cat"+[]+>>> match (bounded 2 5 "cat") "catcatcatcatcatcat"+[]++`bounded` could be implemented naively as follows:++> bounded m n p = do+> x <- choice (map pure [m..n])+> count x p++-}++bounded :: Int -> Int -> Pattern a -> Pattern [a]+bounded m n p+ | m == n = count m p+ | m < n = (++) <$> count m p <*> option (upperBounded (n - m) p)+ | otherwise = mzero++{-| Transform a parser to a succeed with an empty value instead of failing++ See also: `optional`++>>> match (option "1" <> "2") "12"+["12"]+>>> match (option "1" <> "2") "2"+["2"]+-}+option :: Monoid a => Pattern a -> Pattern a+option p = p <|> mempty++{-| @(between open close p)@ matches @\'p\'@ in between @\'open\'@ and+ @\'close\'@++>>> match (between (char '(') (char ')') (star anyChar)) "(123)"+["123"]+>>> match (between (char '(') (char ')') (star anyChar)) "(123"+[]+-}+between :: Pattern a -> Pattern b -> Pattern c -> Pattern c+between open close p = open *> p <* close++{-| Discard the pattern's result++>>> match (skip anyChar) "1"+[()]+>>> match (skip anyChar) ""+[]+-}+skip :: Pattern a -> Pattern ()+skip = void++{-| Restrict the pattern to consume no more than the given number of characters++>>> match (within 2 decimal) "12"+[12]+>>> match (within 2 decimal) "1"+[1]+>>> match (within 2 decimal) "123"+[]+-}+within :: Int -> Pattern a -> Pattern a+within n p = Pattern (do+ txt <- get+ let (before, after) = Text.splitAt n txt+ put before+ a <- runPattern p+ modify (<> after)+ return a )++{-| Require the pattern to consume exactly the given number of characters++>>> match (fixed 2 decimal) "12"+[12]+>>> match (fixed 2 decimal) "1"+[]+-}+fixed :: Int -> Pattern a -> Pattern a+fixed n p = do+ txt <- Pattern get+ guard (Text.length txt >= n)+ within n (p <* eof)++{-| @p `sepBy` sep@ matches zero or more occurrences of @p@ separated by @sep@++>>> match (decimal `sepBy` char ',') "1,2,3"+[[1,2,3]]+>>> match (decimal `sepBy` char ',') ""+[[]]+-}+sepBy :: Pattern a -> Pattern b -> Pattern [a]+p `sepBy` sep = (p `sepBy1` sep) <|> pure []++{-| @p `sepBy1` sep@ matches one or more occurrences of @p@ separated by @sep@++>>> match (decimal `sepBy1` ",") "1,2,3"+[[1,2,3]]+>>> match (decimal `sepBy1` ",") ""+[]+-}+sepBy1 :: Pattern a -> Pattern b -> Pattern [a]+p `sepBy1` sep = (:) <$> p <*> many (sep *> p) ++-- | Like @star dot@ or @star anyChar@, except more efficient+chars :: Pattern Text+chars = Pattern (StateT (\txt ->+ reverse (zip (Text.inits txt) (Text.tails txt)) ))++-- | Like @plus dot@ or @plus anyChar@, except more efficient+chars1 :: Pattern Text+chars1 = Text.cons <$> dot <*> chars
src/Turtle/Prelude.hs view
@@ -1,1081 +1,1126 @@-{-# LANGUAGE CPP #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - --- | This module provides a large suite of utilities that resemble Unix --- utilities. --- --- Many of these commands are just existing Haskell commands renamed to match --- their Unix counterparts: --- --- >>> :set -XOverloadedStrings --- >>> cd "/tmp" --- >>> pwd --- FilePath "/tmp" --- --- Some commands are `Shell`s that emit streams of values. `view` prints all --- values in a `Shell` stream: --- --- >>> view (ls "/usr") --- FilePath "/usr/lib" --- FilePath "/usr/src" --- FilePath "/usr/sbin" --- FilePath "/usr/include" --- FilePath "/usr/share" --- FilePath "/usr/games" --- FilePath "/usr/local" --- FilePath "/usr/bin" --- >>> view (find "Browser.py" "/usr/lib") --- FilePath "lib/python3.2/idlelib/ObjectBrowser.py" --- FilePath "lib/python3.2/idlelib/PathBrowser.py" --- FilePath "lib/python3.2/idlelib/RemoteObjectBrowser.py" --- FilePath "lib/python3.2/idlelib/ClassBrowser.py" --- --- Use `fold` to reduce the output of a `Shell` stream: --- --- >>> import qualified Control.Foldl as Fold --- >>> fold (ls "/usr") Fold.length --- 8 --- >>> fold (find "Browser.py" "/usr/lib") Fold.head --- Just (FilePath "/usr/lib/python3.2/idlelib/ObjectBrowser.py") --- --- Create files using `output`: --- --- >>> output "foo.txt" ("123" <|> "456" <|> "ABC") --- >>> realpath "foo.txt" --- FilePath "/tmp/foo.txt" --- --- Read in files using `input`: --- --- >>> stdout (input "foo.txt") --- 123 --- 456 --- ABC --- --- Commands like `grep`, `sed` and `find` accept arbitrary `Pattern`s --- --- >>> stdout (grep ("123" <|> "ABC") (input "foo.txt")) --- 123 --- ABC --- >>> let exclaim = fmap (<> "!") (plus digit) --- >>> stdout (sed exclaim (input "foo.txt")) --- 123! --- 456! --- ABC --- --- Note that `grep` and `find` differ from their Unix counterparts by requiring --- that the `Pattern` matches the entire line or file name by default. However, --- you can optionally match the prefix, suffix, or interior of a line: --- --- >>> stdout (grep (has "2") (input "foo.txt")) --- 123 --- >>> stdout (grep (prefix "1") (input "foo.txt")) --- 123 --- >>> stdout (grep (suffix "3") (input "foo.txt")) --- 123 --- --- You can also build up more sophisticated `Shell` programs using `sh` in --- conjunction with @do@ notation: --- --- >{-# LANGUAGE OverloadedStrings #-} --- > --- >import Turtle --- > --- >main = sh example --- > --- >example = do --- > -- Read in file names from "files1.txt" and "files2.txt" --- > file <- fmap fromText (input "files1.txt" <|> input "files2.txt") --- > --- > -- Stream each file to standard output only if the file exists --- > True <- liftIO (testfile file) --- > line <- input file --- > liftIO (echo line) --- --- See "Turtle.Tutorial" for an extended tutorial explaining how to use this --- library in greater detail. - -module Turtle.Prelude ( - -- * IO - proc - , shell - , procStrict - , shellStrict - , echo - , err - , readline - , arguments -#if MIN_VERSION_base(4,7,0) - , export - , unset -#endif -#if MIN_VERSION_base(4,6,0) - , need -#endif - , env - , cd - , pwd - , home - , realpath - , mv - , mkdir - , mktree - , cp - , rm - , rmdir - , rmtree - , testfile - , testdir - , date - , datefile - , touch - , time - , hostname - , sleep - , exit - , die - , (.&&.) - , (.||.) - - -- * Managed - , readonly - , writeonly - , appendonly - , mktemp - , mktempdir - , fork - , wait - - -- * Shell - , inproc - , inshell - , stdin - , input - , inhandle - , stdout - , output - , outhandle - , append - , stderr - , strict - , ls - , lstree - , cat - , grep - , sed - , find - , yes - , limit - , limitWhile - , cache - - -- * Folds - , countChars - , countWords - , countLines - - -- * Permissions - , Permissions - , chmod - , getmod - , setmod - , readable, nonreadable - , writable, nonwritable - , 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.Concurrent.Async (Async, withAsync, wait, concurrently) -import Control.Concurrent (threadDelay) -import Control.Exception (bracket, throwIO) -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) -#ifdef mingw32_HOST_OS -import Data.Bits ((.&.)) -#endif -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, -#if MIN_VERSION_base(4,7,0) - setEnv, - unsetEnv, -#endif -#if MIN_VERSION_base(4,6,0) - lookupEnv, -#endif - getEnvironment ) -import System.Directory (Permissions) -import qualified System.Directory as Directory -import System.Exit (ExitCode(..), exitWith) -import System.IO (Handle) -import qualified System.IO as IO -import System.IO.Temp (withTempDirectory, withTempFile) -import qualified System.Process as Process -#ifdef mingw32_HOST_OS -import qualified System.Win32 as Win32 -#else -import System.Posix (openDirStream, readDirStream, closeDirStream, touchFile) -#endif -import Prelude hiding (FilePath) - -import Turtle.Pattern (Pattern, anyChar, match) -import Turtle.Shell -import Turtle.Format (format, w, (%)) - -{-| Run a command using @execvp@, retrieving the exit code - - The command inherits @stdout@ and @stderr@ for the current process --} -proc - :: MonadIO io - => Text - -- ^ Command - -> [Text] - -- ^ Arguments - -> Shell Text - -- ^ Lines of standard input - -> io ExitCode - -- ^ Exit code -proc cmd args = system (Process.proc (unpack cmd) (map unpack args)) - -{-| Run a command line using the shell, retrieving the exit code - - This command is more powerful than `proc`, but highly vulnerable to code - injection if you template the command line with untrusted input - - The command inherits @stdout@ and @stderr@ for the current process --} -shell - :: MonadIO io - => Text - -- ^ Command line - -> Shell Text - -- ^ Lines of standard input - -> io ExitCode - -- ^ Exit code -shell cmdLine = system (Process.shell (unpack cmdLine)) - -{-| Run a command using @execvp@, retrieving the exit code and stdout as a - non-lazy blob of Text - - The command inherits @stderr@ for the current process --} -procStrict - :: MonadIO io - => Text - -- ^ Command - -> [Text] - -- ^ Arguments - -> Shell Text - -- ^ Lines of standard input - -> io (ExitCode, Text) - -- ^ Exit code and stdout -procStrict cmd args = - systemStrict (Process.proc (Text.unpack cmd) (map Text.unpack args)) - -{-| Run a command line using the shell, retrieving the exit code and stdout as a - non-lazy blob of Text - - This command is more powerful than `proc`, but highly vulnerable to code - injection if you template the command line with untrusted input - - The command inherits @stderr@ for the current process --} -shellStrict - :: MonadIO io - => Text - -- ^ Command line - -> Shell Text - -- ^ Lines of standard input - -> io (ExitCode, Text) - -- ^ Exit code and stdout -shellStrict cmdLine = systemStrict (Process.shell (Text.unpack cmdLine)) - -system - :: MonadIO io - => Process.CreateProcess - -- ^ Command - -> Shell Text - -- ^ Lines of standard input - -> io ExitCode - -- ^ Exit code -system p s = liftIO (do - let p' = p - { Process.std_in = Process.CreatePipe - , Process.std_out = Process.Inherit - , Process.std_err = Process.Inherit - } - (Just hIn, Nothing, Nothing, ph) <- liftIO (Process.createProcess p') - let feedIn = sh (do - txt <- s - liftIO (Text.hPutStrLn hIn txt) ) - withAsync feedIn (\a -> liftIO (Process.waitForProcess ph) <* wait a) ) - -systemStrict - :: MonadIO io - => Process.CreateProcess - -- ^ Command - -> Shell Text - -- ^ Lines of standard input - -> io (ExitCode, Text) - -- ^ Exit code and stdout -systemStrict p s = liftIO (do - let p' = p - { Process.std_in = Process.CreatePipe - , Process.std_out = Process.CreatePipe - , Process.std_err = Process.Inherit - } - (Just hIn, Just hOut, Nothing, ph) <- liftIO (Process.createProcess p') - let feedIn = sh (do - txt <- s - liftIO (Text.hPutStrLn hIn txt) ) - concurrently - (withAsync feedIn (\a -> liftIO (Process.waitForProcess ph) <* wait a)) - (Text.hGetContents hOut) ) - -{-| Run a command using @execvp@, streaming @stdout@ as lines of `Text` - - The command inherits @stderr@ for the current process --} -inproc - :: Text - -- ^ Command - -> [Text] - -- ^ Arguments - -> Shell Text - -- ^ Lines of standard input - -> Shell Text - -- ^ Lines of standard output -inproc cmd args = stream (Process.proc (unpack cmd) (map unpack args)) - -{-| Run a command line using the shell, streaming @stdout@ as lines of `Text` - - This command is more powerful than `inproc`, but highly vulnerable to code - injection if you template the command line with untrusted input - - The command inherits @stderr@ for the current process --} -inshell - :: Text - -- ^ Command line - -> Shell Text - -- ^ Lines of standard input - -> Shell Text - -- ^ Lines of standard output -inshell cmd = stream (Process.shell (unpack cmd)) - -stream - :: Process.CreateProcess - -- ^ Command - -> Shell Text - -- ^ Lines of standard input - -> Shell Text - -- ^ Lines of standard output -stream p s = do - let p' = p - { Process.std_in = Process.CreatePipe - , Process.std_out = Process.CreatePipe - , Process.std_err = Process.Inherit - } - (Just hIn, Just hOut, Nothing, _) <- liftIO (Process.createProcess p') - let feedIn = sh (do - txt <- s - liftIO (Text.hPutStrLn hIn txt) ) - a <- using (fork feedIn) - inhandle hOut <|> (liftIO (wait a) *> empty) - --- | Print to @stdout@ -echo :: MonadIO io => Text -> io () -echo txt = liftIO (Text.putStrLn txt) - --- | Print to @stderr@ -err :: MonadIO io => Text -> io () -err txt = liftIO (Text.hPutStrLn IO.stderr txt) - -{-| Read in a line from @stdin@ - - Returns `Nothing` if at end of input --} -readline :: MonadIO io => io (Maybe Text) -readline = liftIO (do - eof <- IO.isEOF - if eof - then return Nothing - else fmap (Just . pack) getLine ) - --- | Get command line arguments in a list -arguments :: MonadIO io => io [Text] -arguments = liftIO (fmap (map pack) getArgs) - -#if MIN_VERSION_base(4,7,0) --- | Set or modify an environment variable -export :: MonadIO io => Text -> Text -> io () -export key val = liftIO (setEnv (unpack key) (unpack val)) - --- | Delete an environment variable -unset :: MonadIO io => Text -> io () -unset key = liftIO (unsetEnv (unpack key)) -#endif - -#if MIN_VERSION_base(4,6,0) --- | Look up an environment variable -need :: MonadIO io => Text -> io (Maybe Text) -need key = liftIO (fmap (fmap pack) (lookupEnv (unpack key))) -#endif - --- | Retrieve all environment variables -env :: MonadIO io => io [(Text, Text)] -env = liftIO (fmap (fmap toTexts) getEnvironment) - where - toTexts (key, val) = (pack key, pack val) - --- | Change the current directory -cd :: MonadIO io => FilePath -> io () -cd path = liftIO (Filesystem.setWorkingDirectory path) - --- | Get the current directory -pwd :: MonadIO io => io FilePath -pwd = liftIO Filesystem.getWorkingDirectory - --- | Get the home directory -home :: MonadIO io => io FilePath -home = liftIO Filesystem.getHomeDirectory - --- | Canonicalize a path -realpath :: MonadIO io => FilePath -> io FilePath -realpath path = liftIO (Filesystem.canonicalizePath path) - -#ifdef mingw32_HOST_OS -fILE_ATTRIBUTE_REPARSE_POINT :: Win32.FileAttributeOrFlag -fILE_ATTRIBUTE_REPARSE_POINT = 1024 - -reparsePoint :: Win32.FileAttributeOrFlag -> Bool -reparsePoint attr = fILE_ATTRIBUTE_REPARSE_POINT .&. attr /= 0 -#endif - -{-| Stream all immediate children of the given directory, excluding @\".\"@ and - @\"..\"@ --} -ls :: FilePath -> Shell FilePath -ls path = Shell (\(FoldM step begin done) -> do - x0 <- begin - let path' = Filesystem.encodeString path - canRead <- fmap - Directory.readable - (Directory.getPermissions (deslash path')) -#ifdef mingw32_HOST_OS - reparse <- fmap reparsePoint (Win32.getFileAttributes path') - if (canRead && not reparse) - then bracket - (Win32.findFirstFile (Filesystem.encodeString (path </> "*"))) - (\(h, _) -> Win32.findClose h) - (\(h, fdat) -> do - let loop x = do - file' <- Win32.getFindDataFileName fdat - let file = Filesystem.decodeString file' - x' <- if (file' /= "." && file' /= "..") - then step x (path </> file) - else return x - more <- Win32.findNextFile h fdat - if more then loop $! x' else done x' - loop $! x0 ) - else done x0 ) -#else - if canRead - then bracket (openDirStream path') closeDirStream (\dirp -> do - let loop x = do - file' <- readDirStream dirp - case file' of - "" -> done x - _ -> do - let file = Filesystem.decodeString file' - x' <- if (file' /= "." && file' /= "..") - then step x (path </> file) - else return x - loop $! x' - loop $! x0 ) - else done x0 ) -#endif - -{-| This is used to remove the trailing slash from a path, because - `getPermissions` will fail if a path ends with a trailing slash --} -deslash :: String -> String -deslash [] = [] -deslash (c0:cs0) = c0:go cs0 - where - go [] = [] - go ['\\'] = [] - go (c:cs) = c:go cs - --- | Stream all recursive descendents of the given directory -lstree :: FilePath -> Shell FilePath -lstree path = do - child <- ls path - isDir <- liftIO (testdir child) - if isDir - then return child <|> lstree child - else return child - --- | Move a file or directory -mv :: MonadIO io => FilePath -> FilePath -> io () -mv oldPath newPath = liftIO (Filesystem.rename oldPath newPath) - -{-| Create a directory - - Fails if the directory is present --} -mkdir :: MonadIO io => FilePath -> io () -mkdir path = liftIO (Filesystem.createDirectory False path) - -{-| Create a directory tree (equivalent to @mkdir -p@) - - Does not fail if the directory is present --} -mktree :: MonadIO io => FilePath -> io () -mktree path = liftIO (Filesystem.createTree path) - --- | Copy a file -cp :: MonadIO io => FilePath -> FilePath -> io () -cp oldPath newPath = liftIO (Filesystem.copyFile oldPath newPath) - --- | Remove a file -rm :: MonadIO io => FilePath -> io () -rm path = liftIO (Filesystem.removeFile path) - --- | Remove a directory -rmdir :: MonadIO io => FilePath -> io () -rmdir path = liftIO (Filesystem.removeDirectory path) - -{-| Remove a directory tree (equivalent to @rm -r@) - - Use at your own risk --} -rmtree :: MonadIO io => FilePath -> io () -rmtree path = liftIO (Filesystem.removeTree path) - --- | Check if a file exists -testfile :: MonadIO io => FilePath -> io Bool -testfile path = liftIO (Filesystem.isFile path) - --- | Check if a directory exists -testdir :: MonadIO io => FilePath -> io Bool -testdir path = liftIO (Filesystem.isDirectory path) - -{-| Touch a file, updating the access and modification times to the current time - - Creates an empty file if it does not exist --} -touch :: MonadIO io => FilePath -> io () -touch file = do - exists <- testfile file - liftIO (if exists -#ifdef mingw32_HOST_OS - then do - handle <- Win32.createFile - (Filesystem.encodeString file) - Win32.gENERIC_WRITE - Win32.fILE_SHARE_NONE - Nothing - Win32.oPEN_EXISTING - Win32.fILE_ATTRIBUTE_NORMAL - Nothing - (creationTime, _, _) <- Win32.getFileTime handle - systemTime <- Win32.getSystemTimeAsFileTime - Win32.setFileTime handle creationTime systemTime systemTime -#else - then touchFile (Filesystem.encodeString file) -#endif - else output file empty ) - -{-| Update a file or directory's user permissions - -> chmod rwo "foo.txt" -- chmod u=rw foo.txt -> chmod executable "foo.txt" -- chmod u+x foo.txt -> chmod nonwritable "foo.txt" -- chmod u-x foo.txt --} -chmod - :: MonadIO io - => (Permissions -> Permissions) - -- ^ Permissions update function - -> FilePath - -- ^ Path - -> io Permissions - -- ^ Updated permissions -chmod modifyPermissions path = liftIO (do - let path' = deslash (Filesystem.encodeString path) - permissions <- Directory.getPermissions path' - let permissions' = modifyPermissions permissions - changed = permissions /= permissions' - when changed (Directory.setPermissions path' permissions') - return permissions' ) - --- | Get a file or directory's user permissions -getmod :: MonadIO io => FilePath -> io Permissions -getmod path = liftIO (do - let path' = deslash (Filesystem.encodeString path) - Directory.getPermissions path' ) - --- | Set a file or directory's user permissions -setmod :: MonadIO io => Permissions -> FilePath -> io () -setmod permissions path = liftIO (do - let path' = deslash (Filesystem.encodeString path) - Directory.setPermissions path' permissions ) - --- | @+r@ -readable :: Permissions -> Permissions -readable = Directory.setOwnerReadable True - --- | @-r@ -nonreadable :: Permissions -> Permissions -nonreadable = Directory.setOwnerReadable False - --- | @+w@ -writable :: Permissions -> Permissions -writable = Directory.setOwnerWritable True - --- | @-w@ -nonwritable :: Permissions -> Permissions -nonwritable = Directory.setOwnerWritable False - --- | @+x@ -executable :: Permissions -> Permissions -executable = Directory.setOwnerExecutable True - --- | @-x@ -nonexecutable :: Permissions -> Permissions -nonexecutable = Directory.setOwnerExecutable False - --- | @+s@ -searchable :: Permissions -> Permissions -searchable = Directory.setOwnerSearchable True - --- | @-s@ -nonsearchable :: Permissions -> Permissions -nonsearchable = Directory.setOwnerSearchable False - --- | @-r -w -x@ -ooo :: Permissions -> Permissions -ooo = const Directory.emptyPermissions - --- | @+r -w -x@ -roo :: Permissions -> Permissions -roo = readable . ooo - --- | @-r +w -x@ -owo :: Permissions -> Permissions -owo = writable . ooo - --- | @-r -w +x@ -oox :: Permissions -> Permissions -oox = executable . ooo - --- | @-r -w +s@ -oos :: Permissions -> Permissions -oos = searchable . ooo - --- | @+r +w -x@ -rwo :: Permissions -> Permissions -rwo = readable . writable . ooo - --- | @+r -w +x@ -rox :: Permissions -> Permissions -rox = readable . executable . ooo - --- | @+r -w +s@ -ros :: Permissions -> Permissions -ros = readable . searchable . ooo - --- | @-r +w +x@ -owx :: Permissions -> Permissions -owx = writable . executable . ooo - --- | @+r +w +x@ -rwx :: Permissions -> Permissions -rwx = readable . writable . executable . ooo - -rws :: Permissions -> Permissions -rws = readable . writable . searchable . ooo - -{-| Time how long a command takes in monotonic wall clock time - - Returns the duration alongside the return value --} -time :: MonadIO io => io a -> io (a, NominalDiffTime) -time io = do - TimeSpec seconds1 nanoseconds1 <- liftIO (getTime Monotonic) - a <- io - TimeSpec seconds2 nanoseconds2 <- liftIO (getTime Monotonic) - let t = fromIntegral ( seconds2 - seconds1) - + 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, - @(sleep 2.0)@ will sleep for two seconds. --} -sleep :: MonadIO io => NominalDiffTime -> io () -sleep n = liftIO (threadDelay (truncate (n * 10^(6::Int)))) - -{-| Exit with the given exit code - - An exit code of @0@ indicates success --} -exit :: MonadIO io => ExitCode -> io a -exit code = liftIO (exitWith code) - --- | Throw an exception using the provided `Text` message -die :: MonadIO io => Text -> io a -die txt = liftIO (throwIO (userError (unpack txt))) - -infixr 2 .&&., .||. - -{-| Analogous to `&&` in Bash - - Runs the second command only if the first one returns `ExitSuccess` --} -(.&&.) :: IO ExitCode -> IO ExitCode -> IO ExitCode -cmd1 .&&. cmd2 = do - r <- cmd1 - case r of - ExitSuccess -> cmd2 - _ -> return r - -{-| Analogous to `||` in Bash - - Run the second command only if the first one returns `ExitFailure` --} -(.||.) :: IO ExitCode -> IO ExitCode -> IO ExitCode -cmd1 .||. cmd2 = do - r <- cmd1 - case r of - ExitFailure _ -> cmd2 - _ -> return r - -{-| Create a temporary directory underneath the given directory - - Deletes the temporary directory when done --} -mktempdir - :: FilePath - -- ^ Parent directory - -> Text - -- ^ Directory name template - -> Managed FilePath -mktempdir parent prefix = do - let parent' = Filesystem.encodeString parent - let prefix' = unpack prefix - dir' <- managed (withTempDirectory parent' prefix') - return (Filesystem.decodeString dir') - -{-| Create a temporary file underneath the given directory - - Deletes the temporary file when done --} -mktemp - :: FilePath - -- ^ Parent directory - -> Text - -- ^ File name template - -> Managed (FilePath, Handle) -mktemp parent prefix = do - let parent' = Filesystem.encodeString parent - let prefix' = unpack prefix - (file', handle) <- managed (\k -> - withTempFile parent' prefix' (\file' handle -> k (file', handle)) ) - let file = Filesystem.decodeString file' - return (file, handle) - --- | Fork a thread, acquiring an `Async` value -fork :: IO a -> Managed (Async a) -fork io = managed (withAsync io) - --- | Read lines of `Text` from standard input -stdin :: Shell Text -stdin = inhandle IO.stdin - --- | Read lines of `Text` from a file -input :: FilePath -> Shell Text -input file = do - handle <- using (readonly file) - inhandle handle - --- | Read lines of `Text` from a `Handle` -inhandle :: Handle -> Shell Text -inhandle handle = Shell (\(FoldM step begin done) -> do - x0 <- begin - let loop x = do - eof <- IO.hIsEOF handle - if eof - then done x - else do - txt <- Text.hGetLine handle - x' <- step x txt - loop $! x' - loop $! x0 ) - --- | Stream lines of `Text` to standard output -stdout :: MonadIO io => Shell Text -> io () -stdout s = sh (do - txt <- s - liftIO (echo txt) ) - --- | Stream lines of `Text` to a file -output :: MonadIO io => FilePath -> Shell Text -> io () -output file s = sh (do - handle <- using (writeonly file) - 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 - handle <- using (appendonly file) - 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) - --- | Acquire a `Managed` read-only `Handle` from a `FilePath` -readonly :: FilePath -> Managed Handle -readonly file = managed (Filesystem.withTextFile file IO.ReadMode) - --- | Acquire a `Managed` write-only `Handle` from a `FilePath` -writeonly :: FilePath -> Managed Handle -writeonly file = managed (Filesystem.withTextFile file IO.WriteMode) - --- | Acquire a `Managed` append-only `Handle` from a `FilePath` -appendonly :: FilePath -> Managed Handle -appendonly file = managed (Filesystem.withTextFile file IO.AppendMode) - --- | Combine the output of multiple `Shell`s, in order -cat :: [Shell a] -> Shell a -cat = msum - --- | Keep all lines that match the given `Pattern` -grep :: Pattern a -> Shell Text -> Shell Text -grep pattern s = do - txt <- s - _:_ <- return (match pattern txt) - return txt - -{-| Replace all occurrences of a `Pattern` with its `Text` result - - Warning: Do not use a `Pattern` that matches the empty string, since it will - match an infinite number of times --} -sed :: Pattern Text -> Shell Text -> Shell Text -sed pattern s = do - let pattern' = fmap Text.concat - (many (pattern <|> fmap Text.singleton anyChar)) - txt <- s - txt':_ <- return (match pattern' txt) - return txt' - --- | Search a directory recursively for all files matching the given `Pattern` -find :: Pattern a -> FilePath -> Shell FilePath -find pattern dir = do - path <- lstree dir - Right txt <- return (Filesystem.toText path) - _:_ <- return (match pattern txt) - return path - --- | A Stream of @\"y\"@s -yes :: Shell Text -yes = Shell (\(FoldM step begin _) -> do - x0 <- begin - let loop x = do - x' <- step x "y" - loop $! x' - loop $! x0 ) - --- | Limit a `Shell` to a fixed number of values -limit :: Int -> Shell a -> Shell a -limit n s = Shell (\(FoldM step begin done) -> do - ref <- newIORef 0 -- I feel so dirty - let step' x a = do - n' <- readIORef ref - writeIORef ref (n' + 1) - if n' < n then step x a else return x - foldIO s (FoldM step' begin done) ) - -{-| Limit a `Shell` to values that satisfy the predicate - - This terminates the stream on the first value that does not satisfy the - predicate --} -limitWhile :: (a -> Bool) -> Shell a -> Shell a -limitWhile predicate s = Shell (\(FoldM step begin done) -> do - ref <- newIORef True - let step' x a = do - b <- readIORef ref - let b' = b && predicate a - writeIORef ref b' - 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 - --- | 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 +{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- | This module provides a large suite of utilities that resemble Unix+-- utilities.+--+-- Many of these commands are just existing Haskell commands renamed to match+-- their Unix counterparts:+--+-- >>> :set -XOverloadedStrings+-- >>> cd "/tmp"+-- >>> pwd+-- FilePath "/tmp"+--+-- Some commands are `Shell`s that emit streams of values. `view` prints all+-- values in a `Shell` stream:+--+-- >>> view (ls "/usr")+-- FilePath "/usr/lib"+-- FilePath "/usr/src"+-- FilePath "/usr/sbin"+-- FilePath "/usr/include"+-- FilePath "/usr/share"+-- FilePath "/usr/games"+-- FilePath "/usr/local"+-- FilePath "/usr/bin"+-- >>> view (find (suffix "Browser.py") "/usr/lib")+-- FilePath "/usr/lib/python3.4/idlelib/ClassBrowser.py"+-- FilePath "/usr/lib/python3.4/idlelib/RemoteObjectBrowser.py"+-- FilePath "/usr/lib/python3.4/idlelib/PathBrowser.py"+-- FilePath "/usr/lib/python3.4/idlelib/ObjectBrowser.py"+--+-- Use `fold` to reduce the output of a `Shell` stream:+--+-- >>> import qualified Control.Foldl as Fold+-- >>> fold (ls "/usr") Fold.length+-- 8+-- >>> fold (find (suffix "Browser.py") "/usr/lib") Fold.head+-- Just (FilePath "/usr/lib/python3.4/idlelib/ClassBrowser.py")+--+-- Create files using `output`:+--+-- >>> output "foo.txt" ("123" <|> "456" <|> "ABC")+-- >>> realpath "foo.txt"+-- FilePath "/tmp/foo.txt"+--+-- Read in files using `input`:+--+-- >>> stdout (input "foo.txt")+-- 123+-- 456+-- ABC+--+-- Commands like `grep`, `sed` and `find` accept arbitrary `Pattern`s+--+-- >>> stdout (grep ("123" <|> "ABC") (input "foo.txt"))+-- 123+-- ABC+-- >>> let exclaim = fmap (<> "!") (plus digit)+-- >>> stdout (sed exclaim (input "foo.txt"))+-- 123!+-- 456!+-- ABC+--+-- Note that `grep` and `find` differ from their Unix counterparts by requiring+-- that the `Pattern` matches the entire line or file name by default. However,+-- you can optionally match the prefix, suffix, or interior of a line:+--+-- >>> stdout (grep (has "2") (input "foo.txt"))+-- 123+-- >>> stdout (grep (prefix "1") (input "foo.txt"))+-- 123+-- >>> stdout (grep (suffix "3") (input "foo.txt"))+-- 123+--+-- You can also build up more sophisticated `Shell` programs using `sh` in+-- conjunction with @do@ notation:+--+-- >{-# LANGUAGE OverloadedStrings #-}+-- >+-- >import Turtle+-- >+-- >main = sh example+-- >+-- >example = do+-- > -- Read in file names from "files1.txt" and "files2.txt"+-- > file <- fmap fromText (input "files1.txt" <|> input "files2.txt")+-- >+-- > -- Stream each file to standard output only if the file exists+-- > True <- liftIO (testfile file)+-- > line <- input file+-- > liftIO (echo line)+--+-- See "Turtle.Tutorial" for an extended tutorial explaining how to use this+-- library in greater detail.++module Turtle.Prelude (+ -- * IO+ proc+ , shell+ , procStrict+ , shellStrict+ , echo+ , err+ , readline+ , arguments+#if MIN_VERSION_base(4,7,0)+ , export+ , unset+#endif+#if MIN_VERSION_base(4,6,0)+ , need+#endif+ , env+ , cd+ , pwd+ , home+ , realpath+ , mv+ , mkdir+ , mktree+ , cp+ , rm+ , rmdir+ , rmtree+ , testfile+ , testdir+ , date+ , datefile+ , touch+ , time+ , hostname+ , sleep+ , exit+ , die+ , (.&&.)+ , (.||.)++ -- * Managed+ , readonly+ , writeonly+ , appendonly+ , mktemp+ , mktempdir+ , fork+ , wait++ -- * Shell+ , inproc+ , inshell+ , stdin+ , input+ , inhandle+ , stdout+ , output+ , outhandle+ , append+ , stderr+ , strict+ , ls+ , lstree+ , cat+ , grep+ , sed+ , find+ , yes+ , limit+ , limitWhile+ , cache++ -- * Folds+ , countChars+ , countWords+ , countLines++ -- * Permissions+ , Permissions+ , chmod+ , getmod+ , setmod+ , readable, nonreadable+ , writable, nonwritable+ , 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.Concurrent.Async (Async, withAsync, wait, concurrently)+import Control.Concurrent.MVar (newMVar, modifyMVar_)+import Control.Concurrent (threadDelay)+import Control.Exception (bracket, throwIO)+import Control.Foldl (Fold, FoldM(..), genericLength, handles, list, premap)+import qualified Control.Foldl.Text+import Control.Monad (liftM, msum, when, unless)+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.Managed (Managed, managed)+#ifdef mingw32_HOST_OS+import Data.Bits ((.&.))+#endif+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,+#if MIN_VERSION_base(4,7,0)+ setEnv,+ unsetEnv,+#endif+#if MIN_VERSION_base(4,6,0)+ lookupEnv,+#endif+ getEnvironment )+import System.Directory (Permissions)+import qualified System.Directory as Directory+import System.Exit (ExitCode(..), exitWith)+import System.IO (Handle, hClose)+import qualified System.IO as IO+import System.IO.Temp (withTempDirectory, withTempFile)+import qualified System.Process as Process+#ifdef mingw32_HOST_OS+import qualified System.Win32 as Win32+#else+import System.Posix (openDirStream, readDirStream, closeDirStream, touchFile)+#endif+import Prelude hiding (FilePath)++import Turtle.Pattern (Pattern, anyChar, match)+import Turtle.Shell+import Turtle.Format (format, w, (%))++{-| Run a command using @execvp@, retrieving the exit code++ The command inherits @stdout@ and @stderr@ for the current process+-}+proc+ :: MonadIO io+ => Text+ -- ^ Command+ -> [Text]+ -- ^ Arguments+ -> Shell Text+ -- ^ Lines of standard input+ -> io ExitCode+ -- ^ Exit code+proc cmd args = system (Process.proc (unpack cmd) (map unpack args))++{-| Run a command line using the shell, retrieving the exit code++ This command is more powerful than `proc`, but highly vulnerable to code+ injection if you template the command line with untrusted input++ The command inherits @stdout@ and @stderr@ for the current process+-}+shell+ :: MonadIO io+ => Text+ -- ^ Command line+ -> Shell Text+ -- ^ Lines of standard input+ -> io ExitCode+ -- ^ Exit code+shell cmdLine = system (Process.shell (unpack cmdLine))++{-| Run a command using @execvp@, retrieving the exit code and stdout as a+ non-lazy blob of Text++ The command inherits @stderr@ for the current process+-}+procStrict+ :: MonadIO io+ => Text+ -- ^ Command+ -> [Text]+ -- ^ Arguments+ -> Shell Text+ -- ^ Lines of standard input+ -> io (ExitCode, Text)+ -- ^ Exit code and stdout+procStrict cmd args =+ systemStrict (Process.proc (Text.unpack cmd) (map Text.unpack args))++{-| Run a command line using the shell, retrieving the exit code and stdout as a+ non-lazy blob of Text++ This command is more powerful than `proc`, but highly vulnerable to code+ injection if you template the command line with untrusted input++ The command inherits @stderr@ for the current process+-}+shellStrict+ :: MonadIO io+ => Text+ -- ^ Command line+ -> Shell Text+ -- ^ Lines of standard input+ -> io (ExitCode, Text)+ -- ^ Exit code and stdout+shellStrict cmdLine = systemStrict (Process.shell (Text.unpack cmdLine))++system+ :: MonadIO io+ => Process.CreateProcess+ -- ^ Command+ -> Shell Text+ -- ^ Lines of standard input+ -> io ExitCode+ -- ^ Exit code+system p s = liftIO (do+ let p' = p+ { Process.std_in = Process.CreatePipe+ , Process.std_out = Process.Inherit+ , Process.std_err = Process.Inherit+ }++ let open = do+ (Just hIn, Nothing, Nothing, ph) <- Process.createProcess p'+ return (hIn, ph)++ -- Prevent double close+ mvar <- newMVar False+ let close handle = do+ modifyMVar_ mvar (\finalized -> do+ unless finalized (hClose handle)+ return True )++ bracket open (\(hIn, _) -> close hIn) (\(hIn, ph) -> do+ let feedIn = do+ sh (do+ txt <- s+ liftIO (Text.hPutStrLn hIn txt) )+ close hIn+ withAsync feedIn (\a -> liftIO (Process.waitForProcess ph) <* wait a) ) )++systemStrict+ :: MonadIO io+ => Process.CreateProcess+ -- ^ Command+ -> Shell Text+ -- ^ Lines of standard input+ -> io (ExitCode, Text)+ -- ^ Exit code and stdout+systemStrict p s = liftIO (do+ let p' = p+ { Process.std_in = Process.CreatePipe+ , Process.std_out = Process.CreatePipe+ , Process.std_err = Process.Inherit+ }++ let open = do+ (Just hIn, Just hOut, Nothing, ph) <- liftIO (Process.createProcess p')+ return (hIn, hOut, ph)++ -- Prevent double close+ mvar <- newMVar False+ let close handle = do+ modifyMVar_ mvar (\finalized -> do+ unless finalized (hClose handle)+ return True )++ bracket open (\(hIn, _, _) -> close hIn) (\(hIn, hOut, ph) -> do+ let feedIn = do+ sh (do+ txt <- s+ liftIO (Text.hPutStrLn hIn txt) )+ close hIn++ concurrently+ (withAsync feedIn (\a -> liftIO (Process.waitForProcess ph) <* wait a))+ (Text.hGetContents hOut) ) )++{-| Run a command using @execvp@, streaming @stdout@ as lines of `Text`++ The command inherits @stderr@ for the current process+-}+inproc+ :: Text+ -- ^ Command+ -> [Text]+ -- ^ Arguments+ -> Shell Text+ -- ^ Lines of standard input+ -> Shell Text+ -- ^ Lines of standard output+inproc cmd args = stream (Process.proc (unpack cmd) (map unpack args))++{-| Run a command line using the shell, streaming @stdout@ as lines of `Text`++ This command is more powerful than `inproc`, but highly vulnerable to code+ injection if you template the command line with untrusted input++ The command inherits @stderr@ for the current process+-}+inshell+ :: Text+ -- ^ Command line+ -> Shell Text+ -- ^ Lines of standard input+ -> Shell Text+ -- ^ Lines of standard output+inshell cmd = stream (Process.shell (unpack cmd))++stream+ :: Process.CreateProcess+ -- ^ Command+ -> Shell Text+ -- ^ Lines of standard input+ -> Shell Text+ -- ^ Lines of standard output+stream p s = do+ let p' = p+ { Process.std_in = Process.CreatePipe+ , Process.std_out = Process.CreatePipe+ , Process.std_err = Process.Inherit+ }++ let open = do+ (Just hIn, Just hOut, Nothing, ph) <- liftIO (Process.createProcess p')+ return (hIn, hOut, ph)++ -- Prevent double close+ mvar <- liftIO (newMVar False)+ let close handle = do+ modifyMVar_ mvar (\finalized -> do+ unless finalized (hClose handle)+ return True )++ (hIn, hOut, ph) <- using (managed (bracket open (\(hIn, _, _) -> close hIn)))+ let feedIn = do+ sh (do+ txt <- s+ liftIO (Text.hPutStrLn hIn txt) )+ close hIn++ a <- using (fork feedIn)+ inhandle hOut <|> (liftIO (Process.waitForProcess ph *> wait a) *> empty)++-- | Print to @stdout@+echo :: MonadIO io => Text -> io ()+echo txt = liftIO (Text.putStrLn txt)++-- | Print to @stderr@+err :: MonadIO io => Text -> io ()+err txt = liftIO (Text.hPutStrLn IO.stderr txt)++{-| Read in a line from @stdin@++ Returns `Nothing` if at end of input+-}+readline :: MonadIO io => io (Maybe Text)+readline = liftIO (do+ eof <- IO.isEOF+ if eof+ then return Nothing+ else fmap (Just . pack) getLine )++-- | Get command line arguments in a list+arguments :: MonadIO io => io [Text]+arguments = liftIO (fmap (map pack) getArgs)++#if MIN_VERSION_base(4,7,0)+-- | Set or modify an environment variable+export :: MonadIO io => Text -> Text -> io ()+export key val = liftIO (setEnv (unpack key) (unpack val))++-- | Delete an environment variable+unset :: MonadIO io => Text -> io ()+unset key = liftIO (unsetEnv (unpack key))+#endif++#if MIN_VERSION_base(4,6,0)+-- | Look up an environment variable+need :: MonadIO io => Text -> io (Maybe Text)+need key = liftIO (fmap (fmap pack) (lookupEnv (unpack key)))+#endif++-- | Retrieve all environment variables+env :: MonadIO io => io [(Text, Text)]+env = liftIO (fmap (fmap toTexts) getEnvironment)+ where+ toTexts (key, val) = (pack key, pack val)++-- | Change the current directory+cd :: MonadIO io => FilePath -> io ()+cd path = liftIO (Filesystem.setWorkingDirectory path)++-- | Get the current directory+pwd :: MonadIO io => io FilePath+pwd = liftIO Filesystem.getWorkingDirectory++-- | Get the home directory+home :: MonadIO io => io FilePath+home = liftIO Filesystem.getHomeDirectory++-- | Canonicalize a path+realpath :: MonadIO io => FilePath -> io FilePath+realpath path = liftIO (Filesystem.canonicalizePath path)++#ifdef mingw32_HOST_OS+fILE_ATTRIBUTE_REPARSE_POINT :: Win32.FileAttributeOrFlag+fILE_ATTRIBUTE_REPARSE_POINT = 1024++reparsePoint :: Win32.FileAttributeOrFlag -> Bool+reparsePoint attr = fILE_ATTRIBUTE_REPARSE_POINT .&. attr /= 0+#endif++{-| Stream all immediate children of the given directory, excluding @\".\"@ and+ @\"..\"@+-}+ls :: FilePath -> Shell FilePath+ls path = Shell (\(FoldM step begin done) -> do+ x0 <- begin+ let path' = Filesystem.encodeString path+ canRead <- fmap+ Directory.readable+ (Directory.getPermissions (deslash path'))+#ifdef mingw32_HOST_OS+ reparse <- fmap reparsePoint (Win32.getFileAttributes path')+ if (canRead && not reparse)+ then bracket+ (Win32.findFirstFile (Filesystem.encodeString (path </> "*")))+ (\(h, _) -> Win32.findClose h)+ (\(h, fdat) -> do+ let loop x = do+ file' <- Win32.getFindDataFileName fdat+ let file = Filesystem.decodeString file'+ x' <- if (file' /= "." && file' /= "..")+ then step x (path </> file)+ else return x+ more <- Win32.findNextFile h fdat+ if more then loop $! x' else done x'+ loop $! x0 )+ else done x0 )+#else+ if canRead+ then bracket (openDirStream path') closeDirStream (\dirp -> do+ let loop x = do+ file' <- readDirStream dirp+ case file' of+ "" -> done x+ _ -> do+ let file = Filesystem.decodeString file'+ x' <- if (file' /= "." && file' /= "..")+ then step x (path </> file)+ else return x+ loop $! x'+ loop $! x0 )+ else done x0 )+#endif++{-| This is used to remove the trailing slash from a path, because+ `getPermissions` will fail if a path ends with a trailing slash+-}+deslash :: String -> String+deslash [] = []+deslash (c0:cs0) = c0:go cs0+ where+ go [] = []+ go ['\\'] = []+ go (c:cs) = c:go cs++-- | Stream all recursive descendents of the given directory+lstree :: FilePath -> Shell FilePath+lstree path = do+ child <- ls path+ isDir <- liftIO (testdir child)+ if isDir+ then return child <|> lstree child+ else return child++-- | Move a file or directory+mv :: MonadIO io => FilePath -> FilePath -> io ()+mv oldPath newPath = liftIO (Filesystem.rename oldPath newPath)++{-| Create a directory++ Fails if the directory is present+-}+mkdir :: MonadIO io => FilePath -> io ()+mkdir path = liftIO (Filesystem.createDirectory False path)++{-| Create a directory tree (equivalent to @mkdir -p@)++ Does not fail if the directory is present+-}+mktree :: MonadIO io => FilePath -> io ()+mktree path = liftIO (Filesystem.createTree path)++-- | Copy a file+cp :: MonadIO io => FilePath -> FilePath -> io ()+cp oldPath newPath = liftIO (Filesystem.copyFile oldPath newPath)++-- | Remove a file+rm :: MonadIO io => FilePath -> io ()+rm path = liftIO (Filesystem.removeFile path)++-- | Remove a directory+rmdir :: MonadIO io => FilePath -> io ()+rmdir path = liftIO (Filesystem.removeDirectory path)++{-| Remove a directory tree (equivalent to @rm -r@)++ Use at your own risk+-}+rmtree :: MonadIO io => FilePath -> io ()+rmtree path = liftIO (Filesystem.removeTree path)++-- | Check if a file exists+testfile :: MonadIO io => FilePath -> io Bool+testfile path = liftIO (Filesystem.isFile path)++-- | Check if a directory exists+testdir :: MonadIO io => FilePath -> io Bool+testdir path = liftIO (Filesystem.isDirectory path)++{-| Touch a file, updating the access and modification times to the current time++ Creates an empty file if it does not exist+-}+touch :: MonadIO io => FilePath -> io ()+touch file = do+ exists <- testfile file+ liftIO (if exists+#ifdef mingw32_HOST_OS+ then do+ handle <- Win32.createFile+ (Filesystem.encodeString file)+ Win32.gENERIC_WRITE+ Win32.fILE_SHARE_NONE+ Nothing+ Win32.oPEN_EXISTING+ Win32.fILE_ATTRIBUTE_NORMAL+ Nothing+ (creationTime, _, _) <- Win32.getFileTime handle+ systemTime <- Win32.getSystemTimeAsFileTime+ Win32.setFileTime handle creationTime systemTime systemTime+#else+ then touchFile (Filesystem.encodeString file)+#endif+ else output file empty )++{-| Update a file or directory's user permissions++> chmod rwo "foo.txt" -- chmod u=rw foo.txt+> chmod executable "foo.txt" -- chmod u+x foo.txt+> chmod nonwritable "foo.txt" -- chmod u-x foo.txt+-}+chmod+ :: MonadIO io+ => (Permissions -> Permissions)+ -- ^ Permissions update function+ -> FilePath+ -- ^ Path+ -> io Permissions+ -- ^ Updated permissions+chmod modifyPermissions path = liftIO (do+ let path' = deslash (Filesystem.encodeString path)+ permissions <- Directory.getPermissions path'+ let permissions' = modifyPermissions permissions+ changed = permissions /= permissions'+ when changed (Directory.setPermissions path' permissions')+ return permissions' )++-- | Get a file or directory's user permissions+getmod :: MonadIO io => FilePath -> io Permissions+getmod path = liftIO (do+ let path' = deslash (Filesystem.encodeString path)+ Directory.getPermissions path' )++-- | Set a file or directory's user permissions+setmod :: MonadIO io => Permissions -> FilePath -> io ()+setmod permissions path = liftIO (do+ let path' = deslash (Filesystem.encodeString path)+ Directory.setPermissions path' permissions )++-- | @+r@+readable :: Permissions -> Permissions+readable = Directory.setOwnerReadable True++-- | @-r@+nonreadable :: Permissions -> Permissions+nonreadable = Directory.setOwnerReadable False++-- | @+w@+writable :: Permissions -> Permissions+writable = Directory.setOwnerWritable True++-- | @-w@+nonwritable :: Permissions -> Permissions+nonwritable = Directory.setOwnerWritable False++-- | @+x@+executable :: Permissions -> Permissions+executable = Directory.setOwnerExecutable True++-- | @-x@+nonexecutable :: Permissions -> Permissions+nonexecutable = Directory.setOwnerExecutable False++-- | @+s@+searchable :: Permissions -> Permissions+searchable = Directory.setOwnerSearchable True++-- | @-s@+nonsearchable :: Permissions -> Permissions+nonsearchable = Directory.setOwnerSearchable False++-- | @-r -w -x@+ooo :: Permissions -> Permissions+ooo = const Directory.emptyPermissions++-- | @+r -w -x@+roo :: Permissions -> Permissions+roo = readable . ooo++-- | @-r +w -x@+owo :: Permissions -> Permissions+owo = writable . ooo++-- | @-r -w +x@+oox :: Permissions -> Permissions+oox = executable . ooo++-- | @-r -w +s@+oos :: Permissions -> Permissions+oos = searchable . ooo++-- | @+r +w -x@+rwo :: Permissions -> Permissions+rwo = readable . writable . ooo++-- | @+r -w +x@+rox :: Permissions -> Permissions+rox = readable . executable . ooo++-- | @+r -w +s@+ros :: Permissions -> Permissions+ros = readable . searchable . ooo++-- | @-r +w +x@+owx :: Permissions -> Permissions+owx = writable . executable . ooo++-- | @+r +w +x@+rwx :: Permissions -> Permissions+rwx = readable . writable . executable . ooo++rws :: Permissions -> Permissions+rws = readable . writable . searchable . ooo++{-| Time how long a command takes in monotonic wall clock time++ Returns the duration alongside the return value+-}+time :: MonadIO io => io a -> io (a, NominalDiffTime)+time io = do+ TimeSpec seconds1 nanoseconds1 <- liftIO (getTime Monotonic)+ a <- io+ TimeSpec seconds2 nanoseconds2 <- liftIO (getTime Monotonic)+ let t = fromIntegral ( seconds2 - seconds1)+ + 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,+ @(sleep 2.0)@ will sleep for two seconds.+-}+sleep :: MonadIO io => NominalDiffTime -> io ()+sleep n = liftIO (threadDelay (truncate (n * 10^(6::Int))))++{-| Exit with the given exit code++ An exit code of @0@ indicates success+-}+exit :: MonadIO io => ExitCode -> io a+exit code = liftIO (exitWith code)++-- | Throw an exception using the provided `Text` message+die :: MonadIO io => Text -> io a+die txt = liftIO (throwIO (userError (unpack txt)))++infixr 2 .&&., .||.++{-| Analogous to `&&` in Bash++ Runs the second command only if the first one returns `ExitSuccess`+-}+(.&&.) :: IO ExitCode -> IO ExitCode -> IO ExitCode+cmd1 .&&. cmd2 = do+ r <- cmd1+ case r of+ ExitSuccess -> cmd2+ _ -> return r++{-| Analogous to `||` in Bash++ Run the second command only if the first one returns `ExitFailure`+-}+(.||.) :: IO ExitCode -> IO ExitCode -> IO ExitCode+cmd1 .||. cmd2 = do+ r <- cmd1+ case r of+ ExitFailure _ -> cmd2+ _ -> return r++{-| Create a temporary directory underneath the given directory++ Deletes the temporary directory when done+-}+mktempdir+ :: FilePath+ -- ^ Parent directory+ -> Text+ -- ^ Directory name template+ -> Managed FilePath+mktempdir parent prefix = do+ let parent' = Filesystem.encodeString parent+ let prefix' = unpack prefix+ dir' <- managed (withTempDirectory parent' prefix')+ return (Filesystem.decodeString dir')++{-| Create a temporary file underneath the given directory++ Deletes the temporary file when done+-}+mktemp+ :: FilePath+ -- ^ Parent directory+ -> Text+ -- ^ File name template+ -> Managed (FilePath, Handle)+mktemp parent prefix = do+ let parent' = Filesystem.encodeString parent+ let prefix' = unpack prefix+ (file', handle) <- managed (\k ->+ withTempFile parent' prefix' (\file' handle -> k (file', handle)) )+ let file = Filesystem.decodeString file'+ return (file, handle)++-- | Fork a thread, acquiring an `Async` value+fork :: IO a -> Managed (Async a)+fork io = managed (withAsync io)++-- | Read lines of `Text` from standard input+stdin :: Shell Text+stdin = inhandle IO.stdin++-- | Read lines of `Text` from a file+input :: FilePath -> Shell Text+input file = do+ handle <- using (readonly file)+ inhandle handle++-- | Read lines of `Text` from a `Handle`+inhandle :: Handle -> Shell Text+inhandle handle = Shell (\(FoldM step begin done) -> do+ x0 <- begin+ let loop x = do+ eof <- IO.hIsEOF handle+ if eof+ then done x+ else do+ txt <- Text.hGetLine handle+ x' <- step x txt+ loop $! x'+ loop $! x0 )++-- | Stream lines of `Text` to standard output+stdout :: MonadIO io => Shell Text -> io ()+stdout s = sh (do+ txt <- s+ liftIO (echo txt) )++-- | Stream lines of `Text` to a file+output :: MonadIO io => FilePath -> Shell Text -> io ()+output file s = sh (do+ handle <- using (writeonly file)+ 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+ handle <- using (appendonly file)+ 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)++-- | Acquire a `Managed` read-only `Handle` from a `FilePath`+readonly :: FilePath -> Managed Handle+readonly file = managed (Filesystem.withTextFile file IO.ReadMode)++-- | Acquire a `Managed` write-only `Handle` from a `FilePath`+writeonly :: FilePath -> Managed Handle+writeonly file = managed (Filesystem.withTextFile file IO.WriteMode)++-- | Acquire a `Managed` append-only `Handle` from a `FilePath`+appendonly :: FilePath -> Managed Handle+appendonly file = managed (Filesystem.withTextFile file IO.AppendMode)++-- | Combine the output of multiple `Shell`s, in order+cat :: [Shell a] -> Shell a+cat = msum++-- | Keep all lines that match the given `Pattern`+grep :: Pattern a -> Shell Text -> Shell Text+grep pattern s = do+ txt <- s+ _:_ <- return (match pattern txt)+ return txt++{-| Replace all occurrences of a `Pattern` with its `Text` result++ Warning: Do not use a `Pattern` that matches the empty string, since it will+ match an infinite number of times+-}+sed :: Pattern Text -> Shell Text -> Shell Text+sed pattern s = do+ let pattern' = fmap Text.concat+ (many (pattern <|> fmap Text.singleton anyChar))+ txt <- s+ txt':_ <- return (match pattern' txt)+ return txt'++-- | Search a directory recursively for all files matching the given `Pattern`+find :: Pattern a -> FilePath -> Shell FilePath+find pattern dir = do+ path <- lstree dir+ Right txt <- return (Filesystem.toText path)+ _:_ <- return (match pattern txt)+ return path++-- | A Stream of @\"y\"@s+yes :: Shell Text+yes = Shell (\(FoldM step begin _) -> do+ x0 <- begin+ let loop x = do+ x' <- step x "y"+ loop $! x'+ loop $! x0 )++-- | Limit a `Shell` to a fixed number of values+limit :: Int -> Shell a -> Shell a+limit n s = Shell (\(FoldM step begin done) -> do+ ref <- newIORef 0 -- I feel so dirty+ let step' x a = do+ n' <- readIORef ref+ writeIORef ref (n' + 1)+ if n' < n then step x a else return x+ foldIO s (FoldM step' begin done) )++{-| Limit a `Shell` to values that satisfy the predicate++ This terminates the stream on the first value that does not satisfy the+ predicate+-}+limitWhile :: (a -> Bool) -> Shell a -> Shell a+limitWhile predicate s = Shell (\(FoldM step begin done) -> do+ ref <- newIORef True+ let step' x a = do+ b <- readIORef ref+ let b' = b && predicate a+ writeIORef ref b'+ 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++-- | 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/Shell.hs view
@@ -1,173 +1,173 @@-{-# LANGUAGE RankNTypes #-} -{-# OPTIONS_GHC -fno-warn-missing-methods #-} - -{-| You can think of `Shell` as @[]@ + `IO` + `Managed`. In fact, you can embed - all three of them within a `Shell`: - -> select :: [a] -> Shell a -> liftIO :: IO a -> Shell a -> using :: Managed a -> Shell a - - Those three embeddings obey these laws: - -> do { x <- select m; select (f x) } = select (do { x <- m; f x }) -> do { x <- liftIO m; liftIO (f x) } = liftIO (do { x <- m; f x }) -> do { x <- with m; using (f x) } = using (do { x <- m; f x }) -> -> select (return x) = return x -> liftIO (return x) = return x -> using (return x) = return x - - ... and `select` obeys these additional laws: - -> select xs <|> select ys = select (xs <|> ys) -> select empty = empty - - You typically won't build `Shell`s using the `Shell` constructor. Instead, - use these functions to generate primitive `Shell`s: - - * `empty`, to create a `Shell` that outputs nothing - - * `return`, to create a `Shell` that outputs a single value - - * `select`, to range over a list of values within a `Shell` - - * `liftIO`, to embed an `IO` action within a `Shell` - - * `using`, to acquire a `Managed` resource within a `Shell` - - Then use these classes to combine those primitive `Shell`s into larger - `Shell`s: - - * `Alternative`, to concatenate `Shell` outputs using (`<|>`) - - * `Monad`, to build `Shell` comprehensions using @do@ notation - - If you still insist on building your own `Shell` from scratch, then the - `Shell` you build must satisfy this law: - -> -- For every shell `s`: -> _foldIO s (FoldM step begin done) = do -> x <- begin -> x' <- _foldIO s (FoldM step (return x) return) -> done x' - - ... which is a fancy way of saying that your `Shell` must call @\'begin\'@ - exactly once when it begins and call @\'done\'@ exactly once when it ends. --} - -module Turtle.Shell ( - -- * Shell - Shell(..) - , foldIO - , fold - , sh - , view - - -- * Embeddings - , select - , liftIO - , using - ) where - -import Control.Applicative (Applicative(..), Alternative(..), liftA2) -import Control.Monad (MonadPlus(..), ap) -import Control.Monad.IO.Class (MonadIO(..)) -import Control.Monad.Managed (Managed, with) -import Control.Foldl (Fold(..), FoldM(..)) -import qualified Control.Foldl as Foldl -import Data.Monoid (Monoid(..), (<>)) -import Data.String (IsString(..)) - --- | A @(Shell a)@ is a protected stream of @a@'s with side effects -newtype Shell a = Shell { _foldIO :: forall r . FoldM IO a r -> IO r } - --- | Use a @`FoldM` `IO`@ to reduce the stream of @a@'s produced by a `Shell` -foldIO :: MonadIO io => Shell a -> FoldM IO a r -> io r -foldIO s f = liftIO (_foldIO s f) - --- | Use a `Fold` to reduce the stream of @a@'s produced by a `Shell` -fold :: MonadIO io => Shell a -> Fold a b -> io b -fold s f = foldIO s (Foldl.generalize f) - --- | Run a `Shell` to completion, discarding any unused values -sh :: MonadIO io => Shell a -> io () -sh s = fold s (pure ()) - --- | Run a `Shell` to completion, `print`ing any unused values -view :: (MonadIO io, Show a) => Shell a -> io () -view s = sh (do - x <- s - liftIO (print x) ) - -instance Functor Shell where - fmap f s = Shell (\(FoldM step begin done) -> - let step' x a = step x (f a) - in _foldIO s (FoldM step' begin done) ) - -instance Applicative Shell where - pure = return - (<*>) = ap - -instance Monad Shell where - return a = Shell (\(FoldM step begin done) -> do - x <- begin - x' <- step x a - done x' ) - - m >>= f = Shell (\(FoldM step0 begin0 done0) -> do - let step1 x a = _foldIO (f a) (FoldM step0 (return x) return) - _foldIO m (FoldM step1 begin0 done0) ) - - fail _ = mzero - -instance Alternative Shell where - empty = Shell (\(FoldM _ begin done) -> do - x <- begin - done x ) - - s1 <|> s2 = Shell (\(FoldM step begin done) -> do - x <- _foldIO s1 (FoldM step begin return) - _foldIO s2 (FoldM step (return x) done) ) - -instance MonadPlus Shell where - mzero = empty - - mplus = (<|>) - -instance MonadIO Shell where - liftIO io = Shell (\(FoldM step begin done) -> do - x <- begin - a <- io - x' <- step x a - done x' ) - -instance Monoid a => Monoid (Shell a) where - mempty = pure mempty - mappend = liftA2 mappend - --- | Shell forms a semiring, this is the closest approximation -instance Monoid a => Num (Shell a) where - fromInteger n = select (replicate (fromInteger n) mempty) - - (+) = (<|>) - (*) = (<>) - -instance IsString a => IsString (Shell a) where - fromString str = pure (fromString str) - --- | Convert a list to a `Shell` that emits each element of the list -select :: [a] -> Shell a -select as = Shell (\(FoldM step begin done) -> do - x0 <- begin - let step' a k x = do - x' <- step x a - k $! x' - foldr step' done as $! x0 ) - --- | Acquire a `Managed` resource within a `Shell` in an exception-safe way -using :: Managed a -> Shell a -using resource = Shell (\(FoldM step begin done) -> do - x <- begin - x' <- with resource (step x) - done x' ) +{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -fno-warn-missing-methods #-}++{-| You can think of `Shell` as @[]@ + `IO` + `Managed`. In fact, you can embed+ all three of them within a `Shell`:++> select :: [a] -> Shell a+> liftIO :: IO a -> Shell a+> using :: Managed a -> Shell a++ Those three embeddings obey these laws:++> do { x <- select m; select (f x) } = select (do { x <- m; f x })+> do { x <- liftIO m; liftIO (f x) } = liftIO (do { x <- m; f x })+> do { x <- with m; using (f x) } = using (do { x <- m; f x })+>+> select (return x) = return x+> liftIO (return x) = return x+> using (return x) = return x++ ... and `select` obeys these additional laws:++> select xs <|> select ys = select (xs <|> ys)+> select empty = empty++ You typically won't build `Shell`s using the `Shell` constructor. Instead,+ use these functions to generate primitive `Shell`s:++ * `empty`, to create a `Shell` that outputs nothing++ * `return`, to create a `Shell` that outputs a single value++ * `select`, to range over a list of values within a `Shell`++ * `liftIO`, to embed an `IO` action within a `Shell`++ * `using`, to acquire a `Managed` resource within a `Shell`+ + Then use these classes to combine those primitive `Shell`s into larger+ `Shell`s:++ * `Alternative`, to concatenate `Shell` outputs using (`<|>`)++ * `Monad`, to build `Shell` comprehensions using @do@ notation++ If you still insist on building your own `Shell` from scratch, then the+ `Shell` you build must satisfy this law:++> -- For every shell `s`:+> _foldIO s (FoldM step begin done) = do+> x <- begin+> x' <- _foldIO s (FoldM step (return x) return)+> done x'++ ... which is a fancy way of saying that your `Shell` must call @\'begin\'@+ exactly once when it begins and call @\'done\'@ exactly once when it ends.+-}++module Turtle.Shell (+ -- * Shell+ Shell(..)+ , foldIO+ , fold+ , sh+ , view++ -- * Embeddings+ , select+ , liftIO+ , using+ ) where++import Control.Applicative (Applicative(..), Alternative(..), liftA2)+import Control.Monad (MonadPlus(..), ap)+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.Managed (Managed, with)+import Control.Foldl (Fold(..), FoldM(..))+import qualified Control.Foldl as Foldl+import Data.Monoid (Monoid(..), (<>))+import Data.String (IsString(..))++-- | A @(Shell a)@ is a protected stream of @a@'s with side effects+newtype Shell a = Shell { _foldIO :: forall r . FoldM IO a r -> IO r }++-- | Use a @`FoldM` `IO`@ to reduce the stream of @a@'s produced by a `Shell`+foldIO :: MonadIO io => Shell a -> FoldM IO a r -> io r+foldIO s f = liftIO (_foldIO s f)++-- | Use a `Fold` to reduce the stream of @a@'s produced by a `Shell`+fold :: MonadIO io => Shell a -> Fold a b -> io b+fold s f = foldIO s (Foldl.generalize f)++-- | Run a `Shell` to completion, discarding any unused values+sh :: MonadIO io => Shell a -> io ()+sh s = fold s (pure ())++-- | Run a `Shell` to completion, `print`ing any unused values+view :: (MonadIO io, Show a) => Shell a -> io ()+view s = sh (do+ x <- s+ liftIO (print x) )++instance Functor Shell where+ fmap f s = Shell (\(FoldM step begin done) ->+ let step' x a = step x (f a)+ in _foldIO s (FoldM step' begin done) )++instance Applicative Shell where+ pure = return+ (<*>) = ap++instance Monad Shell where+ return a = Shell (\(FoldM step begin done) -> do+ x <- begin+ x' <- step x a+ done x' )++ m >>= f = Shell (\(FoldM step0 begin0 done0) -> do+ let step1 x a = _foldIO (f a) (FoldM step0 (return x) return)+ _foldIO m (FoldM step1 begin0 done0) )++ fail _ = mzero++instance Alternative Shell where+ empty = Shell (\(FoldM _ begin done) -> do+ x <- begin+ done x )++ s1 <|> s2 = Shell (\(FoldM step begin done) -> do+ x <- _foldIO s1 (FoldM step begin return)+ _foldIO s2 (FoldM step (return x) done) )++instance MonadPlus Shell where+ mzero = empty++ mplus = (<|>)++instance MonadIO Shell where+ liftIO io = Shell (\(FoldM step begin done) -> do+ x <- begin+ a <- io+ x' <- step x a+ done x' )++instance Monoid a => Monoid (Shell a) where+ mempty = pure mempty+ mappend = liftA2 mappend++-- | Shell forms a semiring, this is the closest approximation+instance Monoid a => Num (Shell a) where+ fromInteger n = select (replicate (fromInteger n) mempty)++ (+) = (<|>)+ (*) = (<>)++instance IsString a => IsString (Shell a) where+ fromString str = pure (fromString str)++-- | Convert a list to a `Shell` that emits each element of the list+select :: [a] -> Shell a+select as = Shell (\(FoldM step begin done) -> do+ x0 <- begin+ let step' a k x = do+ x' <- step x a+ k $! x'+ foldr step' done as $! x0 )++-- | Acquire a `Managed` resource within a `Shell` in an exception-safe way+using :: Managed a -> Shell a+using resource = Shell (\(FoldM step begin done) -> do+ x <- begin+ x' <- with resource (step x)+ done x' )
src/Turtle/Tutorial.hs view
@@ -1,1579 +1,1604 @@-{-# OPTIONS_GHC -fno-warn-unused-imports #-} - -{-| Use @turtle@ if you want to write light-weight and maintainable shell - scripts. - - @turtle@ embeds shell scripting directly within Haskell for three main - reasons: - - * Haskell code is easy to refactor and maintain because the language is - statically typed - - * Haskell is syntactically lightweight, thanks to global type inference - - * Haskell programs can be type-checked and interpreted very rapidly (< 1 - second) - - These features make Haskell ideal for scripting, particularly for replacing - large and unwieldy Bash scripts. - - This tutorial introduces how to use the @turtle@ library to write Haskell - scripts. This assumes no prior knowledge of Haskell, but does assume prior - knowledge of Bash or a similar shell scripting language. - - If you are already proficient with Haskell, then you can get quickly up to - speed by reading the Quick Start guide at the top of "Turtle.Prelude". - - To follow along with the examples, install the Haskell Platform: - - <http://www.haskell.org/platform/> - - ... and then install the @turtle@ library by running: - -> $ cabal install turtle --} - -module Turtle.Tutorial ( - -- * Introduction - -- $introduction - - -- * Comparison - -- $compare - - -- * Subroutines - -- $do - - -- * Types - -- $types - - -- * Shell - -- $shell - - -- * Type signatures - -- $signatures - - -- * System - -- $system - - -- * String formatting - -- $format - - -- * Streams - -- $streams - - -- * Loops - -- $loops - - -- * Folds - -- $folds - - -- * Input and output - -- $io - - -- * External commands - -- $external - - -- * Patterns - -- $patterns - - -- * Exception Safety - -- $exceptions - - -- * MonadIO - -- $monadio - - -- * Command line options - -- $cmdline - - -- * Conclusion - -- $conclusion - ) where - -import Turtle - --- $introduction --- Let's translate some simple Bash scripts to Haskell and work our way up to --- more complex scripts. Here is an example \"Hello, world!\" script written --- in both languages: --- --- @ --- #!\/usr\/bin\/env runhaskell --- -- #!\/bin\/bash --- {-\# LANGUAGE OverloadedStrings \#-} -- --- -- --- import "Turtle" -- --- -- --- main = `echo` \"Hello, world!\" -- echo Hello, world! --- @ --- --- In Haskell you can use @--@ to comment out the rest of a line. The above --- example uses comments to show the equivalent Bash script side-by-side with --- the Haskell script. --- --- You can execute the above code by saving it to the file @example.hs@. If you --- are copying and pasting the code, then remove the leading 1-space indent. --- After you save the file, make the script executable and run the script: --- --- > $ chmod u+x example.hs --- > $ ./example.hs --- > Hello, world! --- --- If you delete the first line of the program, you can also compile the above --- code to generate a native executable which will have a much faster startup --- time and improved performance: --- --- > $ # `-O2` turns on all optimizations --- > $ # `-threaded` helps with piping shell output in and out of Haskell --- > $ ghc -O2 -threaded example.hs --- > $ ./example --- > Hello, world! --- --- You can even run Haskell code interactively using @ghci@, which is an --- interactive REPL for Haskell. You can either use @ghci@ by itself: --- --- > $ ghci --- > <ghci links in some libraries> --- > Prelude> :set -XOverloadedStrings --- > Prelude> import Turtle --- > Prelude Turtle> echo "Hello, world!" --- > <ghci links in some libraries> --- > Hello, world! --- > Prelude Turtle> :quit --- > $ --- --- ... or you can load Haskell code into @ghci@, which will bring all top-level --- values from that program into scope: --- --- > $ ghci example.hs --- > <ghci links in some libraries> --- > [1 of 1] Compiling Main ( example.hs, interpreted ) --- > Ok, modules loaded: Main. --- > *Main> main --- > <ghci links in some libraries> --- > Hello, world! --- > *Main> :quit --- > $ --- --- From now on I'll omit @ghci@'s linker output in tutorial examples. You can --- also silence this linker output by passing the @-v0@ flag to @ghci@. - --- $compare --- You'll already notice a few differences between the Haskell code and Bash --- code. --- --- First, the Haskell code requires two additional lines of overhead to import --- the @turtle@ library and enable overloading of string literals. This --- overhead is mostly unavoidable. --- --- Second, the Haskell `echo` explicitly quotes its string argument whereas the --- Bash @echo@ does not. In Bash every token is a string by default and you --- distinguish variables by prepending a dollar sign to them. In Haskell --- the situation is reversed: every token is a variable by default and you --- distinguish strings by quoting them. The following example highlights the --- difference: --- --- > #!/usr/bin/env runhaskell --- > -- #!/bin/bash --- > {-# LANGUAGE OverloadedStrings #-} -- --- > -- --- > import Turtle -- --- > -- --- > str = "Hello!" --STR=Hello! --- > -- --- > main = echo str --echo $STR --- --- Third, you have to explicitly assign a subroutine to @main@ to specify which --- subroutine to run when your program begins. This is because Haskell lets you --- define things out of order. For example, we could have written our original --- program this way instead: --- --- > #!/usr/bin/env runhaskell --- > --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > --- > main = echo str --- > --- > str = "Hello, world!" --- --- Notice how the above program defines @str@ after @main@, which is valid. --- Haskell does not care in what order you define top-level values or functions --- (using the @=@ sign). However, the top level of a Haskell program only --- permits definitions. If you were to insert a statement at the top-level: --- --- > #!/usr/bin/env runhaskell --- > --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > --- > echo "Hello, world!" --- --- ... then you would get this error when you tried to run your program: --- --- > example.hs:7:1: Parse error: naked expression at top level - --- $do --- You can use @do@ notation to create a subroutine that runs more than one --- command: --- --- > #!/usr/bin/env runhaskell --- > -- #!/bin/bash --- > {-# LANGUAGE OverloadedStrings #-} -- --- > -- --- > import Turtle -- --- > -- --- > main = do -- --- > echo "Line 1" -- echo Line 1 --- > echo "Line 2" -- echo Line 2 --- --- > $ ./example.hs --- > Line 1 --- > Line 2 --- --- @do@ blocks can use either use the indentation level to control their --- duration or they can use curly braces and semicolons. To see the full rules --- for @do@ syntax, read: <http://en.wikibooks.org/wiki/Haskell/Indentation>. --- --- Some commands can return a value, and you can store the result of a command --- using the @<-@ symbol. For example, the following program prints the --- creation time of the current working directory by storing two intermediate --- results: --- --- @ --- #!\/usr\/bin\/env runhaskell --- -- #!\/bin\/bash --- import Turtle -- --- -- --- main = do -- --- dir <- `pwd` -- DIR=$(pwd) --- time <- `datefile` dir -- TIME=$(date -r $DIR) --- `print` time -- echo $TIME --- @ --- --- > $ ./example.hs --- > 2015-01-24 03:40:31 UTC --- --- The main difference between @=@ and @<-@ is that: --- --- * The @<-@ symbol is overloaded and its meaning is context-dependent; in this --- context it just means \"store the current result\" --- --- * The @=@ symbol is not overloaded and always means that the two sides of the --- equality are interchangeable --- --- @do@ notation lets you combine smaller subroutines into larger subroutines. --- For example, we could refactor the above code to split the first two commands --- into their own smaller subroutine and then invoke that smaller subroutine --- within a larger subroutine: --- --- > #!/usr/bin/env runhaskell --- > -- #!/bin/bash --- > import Turtle -- --- > -- --- > datePwd = do -- datePwd() { --- > dir <- pwd -- DIR=$(pwd) --- > result <- datefile dir -- RESULT=$(date -r $DIR) --- > return result -- echo $RESULT --- > -- } --- > main = do -- --- > time <- datePwd -- TIME=$(datePwd) --- > print time -- echo $TIME --- --- The refactored program still returns the exact same result: --- --- > $ ./example.hs --- > 2015-01-24 03:40:31 UTC --- --- We can also simplify the code a little bit because @do@ notation implicitly --- returns the value of the last command within a subroutine. We can use this --- trick to simplify both the Haskell and Bash code: --- --- > datePwd = do -- datePwd() { --- > dir <- pwd -- DIR=$(pwd) --- > datefile dir -- date -r $DIR --- > -- } --- --- However, keep in mind that the `return` statement is something of a misnomer --- since it does not break or exit from the surrounding subroutine. All it --- does is create a trivial subroutine that has no side effects and returns its --- argument as its result. If you `return` an expression, you're just giving --- it a new name: --- --- > do x <- return expr -- X=EXPR --- > command x -- command $X --- > --- > -- Same as: --- > command expr -- command EXPR --- --- In fact, the first line is equivalent to @let x = expr@, which more closely --- mirrors the equivalent Bash syntax: --- --- > do let x = expr -- X=EXPR --- > command x -- command $X --- > --- > -- Same as: --- > command expr -- command EXPR --- --- Also, for a subroutine with a single command, you can omit the @do@: --- --- > main = do echo "Hello, world!" --- > --- > -- Same as: --- > main = echo "Hello, world!" - --- $types --- --- Notice how the above Haskell example used `print` instead of `echo`. Run the --- following script to find out what happens if we choose `echo` instead: --- --- > #!/usr/bin/env runhaskell --- > --- > import Turtle --- > --- > main = do --- > dir <- pwd --- > time <- datefile dir --- > echo time --- --- If we run that we get a type error: --- --- > $ ./example.hs --- > --- > example.hs:8:10: --- > Couldn't match expected type `Text' with actual type `UTCTime' --- > In the first argument of `echo', namely `time' --- > In a stmt of a 'do' block: echo time --- > In the expression: --- > do { dir <- pwd; --- > time <- datefile dir; --- > echo time } --- --- The error points to the last line of our program: @(example.hs:8:10)@ means --- line 8, column 10 of our program. If you study the error message closely --- you'll see that the `echo` function expects a `Text` value, but we passed it --- @\'time\'@, which was a `UTCTime` value. Although the error is at the end of --- our script, Haskell catches this error before even running the script. When --- we \"interpret\" a Haskell script the Haskell compiler actually compiles the --- script without any optimizations to generate a temporary executable and then --- runs the executable, much like Perl does for Perl scripts. --- --- You might wonder: \"where are the types?\" None of the above programs had --- any type signatures or type annotations, yet the compiler still detected type --- errors correctly. This is because Haskell uses \"global type inference\" to --- detect errors, meaning that the compiler can infer the types of expressions --- within the program without any assistance from the programmer. --- --- You can even ask the compiler what the type of an expression is using @ghci@. --- Let's open up the REPL and import this library so that we can study the types --- and deduce why our program failed: --- --- > $ ghci --- > Prelude> import Turtle --- > Prelude Turtle> --- --- You can interrogate the REPL for an expression's type using the @:type@ --- command: --- --- @ --- Prelude Turtle> :type pwd --- pwd :: `MonadIO` io => io Turtle.`Turtle.FilePath` --- @ --- --- For right now, ignore all occurrences of `MonadIO` and just read the type --- as: --- --- @ --- Prelude Turtle> :type pwd --- pwd :: `IO` Turtle.`Turtle.FilePath` --- @ --- --- We will cover `MonadIO` later on. --- --- Whenever you see something of the form @(x :: t)@, that means that @\'x\'@ --- is a value of type @\'t\'@. The REPL says that `pwd` is a subroutine ('IO') --- that returns a `Turtle.FilePath`. The "Turtle" prefix before --- `Turtle.FilePath` is just the module name since the `Turtle.FilePath` --- exported by the @turtle@ library conflicts with the default `FilePath` --- exported by Haskell's @Prelude@. The compiler uses the fully qualified name, --- @"Turtle".`FilePath`@, to avoid ambiguity. --- --- We can similarly ask for the type of `datefile`: --- --- @ --- Prelude Turtle> :type datefile --- datefile :: Turtle.`Turtle.FilePath` -> `IO` `UTCTime` --- @ --- --- `datefile` is a function whose argument must be a `Turtle.FilePath` and whose --- result is a subroutine (`IO`) that returns a `UTCTime`. Notice how the --- input argument of `datefile` (which is a `Turtle.FilePath`) is the same type --- as the return value of `pwd` (also a `Turtle.FilePath`). --- --- Now let's study type of `echo` to see why we get the type error: --- --- @ --- Prelude Turtle> :type echo --- echo :: `Text` -> `IO` () --- @ --- --- The above type says that `echo` is a function whose argument is a value of --- type `Text` and whose result is a subroutine (`IO`) with an empty return --- value (denoted @\'()\'@). --- --- Now we can understand the type error: `echo` expects a `Text` argument but --- `datefile` returns a `UTCTime`, which is not the same thing. Unlike Bash, --- not everything is `Text` in Haskell and the compiler will not cast or coerce --- types for you. --- --- The reason `print` worked is because `print` has a more general type than --- `echo`: --- --- @ --- Prelude Turtle> :type print --- print :: `Show` a => a -> `IO` () --- @ --- --- This type signature says that `print` can display any value of type @\'a\'@ --- so long as @\'a\'@ implements the `Show` interface. In this case `UTCTime` --- does implement the `Show` interface, so everything works out when we use --- `print`. --- --- This library provides a helper function that lets you convert any type that --- implements `Show` into a `Text` value: --- --- @ --- \-\- This behaves like Python's \`repr\` function --- `repr` :: `Show` a => a -> `Text` --- @ --- --- You could therefore implement `print` in terms of `echo` and `repr`: --- --- > print x = echo (repr x) - --- $shell --- --- You can use @ghci@ for more than just inferring types. @ghci@ is a --- general-purpose Haskell shell for your system when you extend it with --- @turtle@: --- --- @ --- $ ghci --- Prelude> :set -XOverloadedStrings --- Prelude> import Turtle --- Prelude Turtle> `cd` \"/tmp\" --- Prelude Turtle> `pwd` --- FilePath \"/tmp\" --- Prelude Turtle> `mkdir` \"test\" --- Prelude Turtle> `cd` \"test\" --- Prelude Turtle> `touch` \"file\" --- Prelude Turtle> `testfile` \"file\" --- True --- Prelude Turtle> `rm` \"file\" --- Prelude Turtle> `testfile` \"file\" --- False --- @ --- --- You can also optionally configure @ghci@ to run the first two commands every --- time you launch @ghci@. Just create a @.ghci@ within your current directory --- with these two lines: --- --- > :set -XOverloadedStrings --- > import Turtle --- --- The following @ghci@ examples will all assume that you run these two commands --- at the beginning of every session, either manually or automatically. You can --- even enable those two commands permanently by adding the above @.ghci@ file --- to your home directory. --- --- Within @ghci@ you can run a subroutine and @ghci@ will `print` the --- subroutine's value if it is not empty: --- --- @ --- Prelude Turtle> `shell` \"true\" empty --- ExitSuccess --- Prelude Turtle> `shell` \"false\" empty --- ExitFailure 1 --- @ --- --- You can also type in a pure expression and @ghci@ will evaluate that --- expression: --- --- @ --- Prelude Turtle> 2 + 2 --- 4 --- Prelude Turtle> \"123\" `<>` \"456\" -- (\<\>) concatenates strings --- \"123456\" --- @ --- --- This works because @ghci@ automatically wraps anything that's not a --- subroutine with `print`. It's as if we had written: --- --- > Prelude Turtle> print (2 + 2) --- > 4 --- > Prelude Turtle> print ("123" <> "456") --- > "123456" - --- $signatures --- --- Haskell performs global type inference, meaning that the compiler never --- requires any type signatures. When you add type signatures, they are purely --- for the benefit of the programmer and behave like machine-checked --- documentation. --- --- Let's illustrate this by adding types to our original script: --- --- > #!/usr/bin/env runhaskell --- > --- > import Turtle --- > --- > datePwd :: IO UTCTime -- Type signature --- > datePwd = do --- > dir <- pwd --- > datefile dir --- > --- > main :: IO () -- Type signature --- > main = do --- > time <- datePwd --- > print time --- --- The first type signature says that @datePwd@ is a subroutine that returns a --- `UTCTime`: --- --- > -- +----- A subroutine ... --- > -- | --- > -- | +-- ... that returns `UTCTime` --- > -- | | --- > -- v v --- > datePwd :: IO UTCTime --- --- The second type signature says that @main@ is a subroutine that returns an --- empty value: --- --- > -- +----- A subroutine ... --- > -- | --- > -- | +-- ... that returns an empty value (i.e. `()`) --- > -- | | --- > -- v v --- > main :: IO () --- --- Not every top-level value has to be a subroutine, though. For example, you --- can define unadorned `Text` values at the top-level, as we saw previously: --- --- > #!/usr/bin/env runhaskell --- > --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > --- > str :: Text --- > str = "Hello!" --- > --- > main :: IO () --- > main = echo str --- --- These type annotations do not assist the compiler. Instead, the compiler --- independently infers the type and then checks whether it matches the --- documented type. If there is a mismatch the compiler will raise a type --- error. --- --- Let's test this out by providing an incorrect type for @\'str\'@: --- --- > #!/usr/bin/env runhaskell --- > --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > --- > str :: Int --- > str = "Hello!" --- > --- > main :: IO () --- > main = echo str --- --- If you run that script, you will get two error messages: --- --- > $ ./example.hs --- > --- > example.hs:8:7: --- > No instance for (IsString Int) --- > arising from the literal `"Hello, world!"' --- > Possible fix: add an instance declaration for (IsString Int) --- > In the expression: "Hello, world!" --- > In an equation for `str': str = "Hello, world!" --- > --- > example.hs:11:13: --- > Couldn't match expected type `Text' with actual type `Int' --- > In the first argument of `echo', namely `str' --- > In the expression: echo str --- > In an equation for `main': main = echo str --- --- The first error message relates to the @OverloadedStrings@ extensions. When --- we enable @OverloadedStrings@ the compiler overloads string literals, --- interpreting them as any type that implements the `IsString` interface. The --- error message says that `Int` does not implement the `IsString` interface so --- the compiler cannot interpret a string literal as an `Int`. On the other --- hand the `Text` and `Turtle.FilePath` types do implement `IsString`, which --- is why we can interpret string literals as `Text` or `Turtle.FilePath` --- values. --- --- The second error message says that `echo` expects a `Text` value, but we --- declared @str@ to be an `Int`, so the compiler aborts compilation, requiring --- us to either fix or delete our type signature. --- --- Notice that there is nothing wrong with the program other than the type --- signature we added. If we were to delete the type signature the program --- would compile and run correctly. The sole purpose of this type signature is --- for us to communicate our expectations to the compiler so that the compiler --- can alert us if the code does not match our expectations. --- --- Let's also try reversing the type error, providing a number where we expect --- a string: --- --- > #!/usr/bin/env runhaskell --- > --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > --- > str :: Text --- > str = 4 --- > --- > main :: IO () --- > main = echo str --- --- This gives a different error: --- --- > $ ./example.hs --- > --- > example.hs:8:7: --- > No instance for (Num Text) --- > arising from the literal `4' --- > Possible fix: add an instance declaration for (Num Text) --- > In the expression: 4 --- > In an equation for `str': str = 4 --- --- Haskell also automatically overloads numeric literals, too. The compiler --- interprets integer literals as any type that implements the `Num` interface. --- The `Text` type does not implement the `Num` interface, so we cannot --- interpret integer literals as `Text` strings. - --- $system --- --- You can invoke arbitrary shell commands using the `shell` command. For --- example, we can write a program that creates an empty directory and then --- uses a `shell` command to archive the directory: --- --- @ --- #!\/usr\/bin\/env runhaskell --- -- #!\/bin\/bash --- {-\# LANGUAGE OverloadedStrings \#-} -- --- -- --- import Turtle -- --- -- --- main = do -- --- mkdir \"test\" -- mkdir test --- `shell` \"tar czf test.tar.gz test\" empty -- tar czf test.tar.gz test --- @ --- --- If you run this program, it will generate the @test.tar.gz@ archive: --- --- > $ ./example.hs --- > ExitSuccess --- > $ echo $? --- > 0 --- > $ ls test.tar.gz --- > test.tar.gz --- --- Like @ghci@, the @runhaskell@ command running our script prints any non-empty --- result of the @main@ subroutine (`ExitSuccess` in this case). --- --- The easiest way to learn a new command like `shell` is to view its --- documentation. Click on the word `shell`, which will take you to --- documentation that looks like this: --- --- @ --- `shell` --- :: Text -- Command line --- -> Shell Text -- Standard input (as lines of \`Text\`) --- -> IO `ExitCode` -- Exit code of the shell command --- @ --- --- The first argument is a `Text` representation of the command to run. The --- second argument lets you feed input to the command, and you can provide --- `empty` for now to feed no input. --- --- The final result is an `ExitCode`, which you can use to detect whether the --- command completed successfully. For example, we could print a more --- descriptive error message if an external command fails: --- --- @ --- #!\/usr\/bin\/env runhaskell --- --- {-\# LANGUAGE OverloadedStrings \#-} --- --- import Turtle --- --- main = do --- let cmd = \"false\" --- x <- shell cmd empty --- case x of --- ExitSuccess -> return () --- ExitFailure n -> `die` (cmd \<\> \" failed with exit code: \" \<\> repr n) --- @ --- --- This prints an error message since the @false@ command always fails: --- --- > $ ./example.hs --- > example.hs: user error (false failed with exit code: 1) --- --- You should also check out the `proc` command, which is less powerful but --- safer since it decreases the likelihood of code injection or malformed --- commands: --- --- @ --- `proc` --- :: Text -- Program --- :: [Text] -- Arguments --- -> Shell Text -- Standard input (as lines of \`Text\`) --- -> IO ExitCode -- Exit code of the shell command --- @ --- --- Most of the commands in this library do not actually invoke an external --- shell or program. Instead, they indirectly wrap other Haskell libraries that --- bind to C code. - --- $format --- --- This library provides type-safe string formatting utilities, too. For --- example, instead of writing this: --- --- > cmd <> " failed with exit code: " <> repr n --- --- ... you could format the string using @printf@ style instead: --- --- > format (s%" failed with exit code: "%d) cmd n --- --- What's neat is that the compiler will automatically infer the number of --- arguments and their types from the `Format` string: --- --- > $ ghci --- > Prelude Turtle> :type format (s%" failed with exit code: "%d) --- > format (s%" failed with exit code: "%d) :: Text -> Int -> Text --- --- The compiler deduces that the above `Format` string requires one argument of --- type `Text` to satisfy the `s` at the beginning of the format string and --- another argument of type `Int` to satisfy the `d` at the end of the format --- string. --- --- If you are interested in this feature, check out the "Turtle.Format" module --- for more details. - --- $streams --- The @turtle@ library provides support for streaming computations, just like --- Bash. The primitive @turtle@ streams are little more verbose than their --- Bash counterparts, but @turtle@ streams can be built and combined in more --- ways. --- --- The key type for streams is the `Shell` type, which represents a stream of --- values. For example, the `ls` function has a streaming result: --- --- @ --- Prelude Turtle> :type `ls` --- `ls` :: Turtle.FilePath -> `Shell` Turtle.FilePath --- @ --- --- That type says that `ls` takes a single `Turtle.FilePath` as its argument --- (the directory to list) and the result is a `Shell` stream of --- `Turtle.FilePath`s (the immediate children of that directory). --- --- You can't run a `Shell` stream directly within @ghci@. You will get a type --- error like this if you try: --- --- > Prelude Turtle> ls "/tmp" --- > --- > <interactive>:2:1: --- > No instance for (Show (Shell Turtle.FilePath)) --- > arising from a use of `print' --- > Possible fix: --- > add an instance declaration for (Show (Shell Turtle.FilePath)) --- > In a stmt of an interactive GHCi command: print it --- --- Instead, you must consume the stream as it is generated and the simplest way --- to consume a `Shell` stream is `view`: --- --- @ --- `view` :: Show a => Shell a -> IO () --- @ --- --- `view` takes any `Shell` stream of values and `print`s them to standard --- output: --- --- > Prelude Turtle> view (ls "/tmp") --- > FilePath "/tmp/.X11-unix" --- > FilePath "/tmp/.X0-lock" --- > FilePath "/tmp/pulse-PKdhtXMmr18n" --- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv" --- > FilePath "/tmp/tracker-gabriel" --- > FilePath "/tmp/pulse-PYi1hSlWgNj2" --- > FilePath "/tmp/orbit-gabriel" --- > FilePath "/tmp/ssh-vREYGbWGpiCa" --- > FilePath "/tmp/.ICE-unix --- --- You can build your own `Shell` streams using a few primitive operations, --- --- The first primitive is `empty`, which represents an empty stream of values: --- --- @ --- Prelude Turtle> view `empty` -- Outputs nothing --- Prelude Turtle> --- @ --- --- Another way to say that is: --- --- @ --- view `empty` = return () --- @ --- --- The type of empty is: --- --- @ --- `empty` :: Shell a --- @ --- --- The lower-case @\'a\'@ is \"polymorphic\", meaning that it will type check as --- any type. That means that you can produce an `empty` stream of any type of --- value. --- --- The next simplest function is `return`, which lets you take any value and --- transform it into a singleton `Shell` that emits just that one value: --- --- @ --- Prelude Turtle> view (`return` 1) --- 1 --- @ --- --- Another way to say that is: --- --- @ --- view (`return` x) = print x --- @ --- --- The type of `return` is: --- --- @ --- `return` :: a -> Shell a --- @ --- --- Notice that this is the same `return` function we saw before. This is --- because `return` is overloaded and works with both `IO` and `Shell`. --- --- You can also take any subroutine ('IO') and transform it into a singleton --- `Shell`: --- --- @ --- Prelude Turtle> view (`liftIO` readline) --- ABC\<Enter\> --- Just \"ABC\" --- @ --- --- Another way to say that is: --- --- @ --- view (`liftIO` io) = do x <- io --- print x --- @ --- --- The type of `liftIO` is: --- --- @ --- `liftIO` :: IO a -> Shell a --- @ --- --- Once you have those primitive `Shell` streams you can begin to combine them --- into larger `Shell` streams. For example, you can concatenate two `Shell` --- streams using (`<|>`): --- --- @ --- view (return 1 `<|>` return 2) --- 1 --- 2 --- @ --- --- Another way to say that is: --- --- @ --- view (xs `<|>` ys) = do view xs --- view ys --- @ --- --- The type of (`<|>`) is: --- --- @ --- (`<|>`) :: Shell a -> Shell a -> Shell a --- @ --- --- In other words, you can concatenate two `Shell` streams of the same element --- type to get a new `Shell` stream, also of the same element type. --- --- Let's try using (`<|>`) on two real streams: --- --- > Prelude Turtle> view (ls "/tmp" <|> ls "/usr") --- > FilePath "/tmp/.X11-unix" --- > FilePath "/tmp/.X0-lock" --- > FilePath "/tmp/pulse-PKdhtXMmr18n" --- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv" --- > FilePath "/tmp/tracker-gabriel" --- > FilePath "/tmp/pulse-PYi1hSlWgNj2" --- > FilePath "/tmp/orbit-gabriel" --- > FilePath "/tmp/ssh-vREYGbWGpiCa" --- > FilePath "/tmp/.ICE-unix" --- > FilePath "/usr/lib" --- > FilePath "/usr/src" --- > FilePath "/usr/sbin" --- > FilePath "/usr/include" --- > FilePath "/usr/share" --- > FilePath "/usr/games" --- > FilePath "/usr/local" --- > FilePath "/usr/bin" --- --- Finally, note that `Shell` implements the `IsString` interface, so a string --- literal will type-check as a `Shell` that emits a single `Text` value: --- --- > Prelude Turtle> view "123" --- > "123" --- > Prelude Turtle> view (return "123") -- Same thing --- > "123" --- > Prelude Turtle> view ("123" <|> "456") --- > "123" --- > "456" --- > Prelude Turtle> view (return "123" <|> return "456") -- Same thing --- > "123" --- > "456" - --- $loops --- --- This library also provides the `select` function for conveniently emitting a --- list of values: --- --- @ --- Prelude Turtle> view (`select` [1, 2, 3]) --- 1 --- 2 --- 3 --- @ --- --- We can use `select` to implement loops within a `Shell`: --- --- > #!/usr/bin/env runhaskell --- > -- #!/bin/bash --- > {-# LANGUAGE OverloadedStrings #-} -- --- > -- --- > import Turtle -- --- > -- --- > example = do -- --- > x <- select [1, 2] -- for x in 1 2; do --- > y <- select [3, 4] -- for y in 3 4; do --- > liftIO (print (x, y)) -- echo \(${x},${y}\); --- > -- done; --- > main = sh example -- done --- --- That will `print` every permutation of @\'x\'@ and @\'y\'@: --- --- > $ ./example --- > (1,3) --- > (1,4) --- > (2,3) --- > (2,4) --- --- This uses the `sh` utility instead of `view`. The only difference is that --- `sh` doesn't print any values (since `print` is doing that already): --- --- @ --- `sh` :: Shell a -> IO () --- @ --- --- This trick isn't limited to `select`. You can loop over the output of any --- `Shell` by just binding its result. For example, this is how `view` loops --- over its argument: --- --- > view :: Show a => Shell a -> IO () --- > view s = sh (do --- > x <- s -- `x` ranges over every output of `s` --- > liftIO (print x) ) --- --- You can also loop over a stream in a one-liner, still using @do@ notation. --- Just insert semi-colons between statements: --- --- > Prelude Turtle> -- for file in /tmp/*; do echo $file; done --- > Prelude Turtle> sh (do file <- ls "/tmp"; liftIO (print file)) --- > FilePath "/tmp/.X11-unix" --- > FilePath "/tmp/.X0-lock" --- > FilePath "/tmp/pulse-PKdhtXMmr18n" --- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv" --- > FilePath "/tmp/tracker-gabriel" --- > FilePath "/tmp/pulse-PYi1hSlWgNj2" --- > FilePath "/tmp/orbit-gabriel" --- > FilePath "/tmp/ssh-vREYGbWGpiCa" --- > FilePath "/tmp/.ICE-unix" - --- $folds --- --- There are other ways you can consume a `Shell` stream. For example, you can --- `fold` the stream using predefined `Fold`s from "Control.Foldl": --- --- @ --- Prelude Turtle> import qualified "Control.Foldl" as Fold --- Prelude Turtle Fold> `fold` (ls \"/tmp\") Fold.length --- 9 --- @ --- --- @ --- Prelude Turtle Fold> `fold` (ls \"/tmp\") Fold.head --- Just (FilePath \"\/tmp\/.X11-unix\") --- @ --- --- @ --- Prelude Turtle Fold> `fold` (ls \"\/tmp\") Fold.list --- [FilePath \"\/tmp\/.X11-unix\",FilePath \"\/tmp\/.X0-lock\",FilePath \"\/tmp\/pulse-PKd --- htXMmr18n\",FilePath \"\/tmp\/pulse-xHYcZ3zmN3Fv\",FilePath \"\/tmp\/tracker-gabriel --- \",FilePath \"\/tmp\/pulse-PYi1hSlWgNj2\",FilePath \"\/tmp\/orbit-gabriel\",FilePath --- \"\/tmp\/ssh-vREYGbWGpiCa\",FilePath \"\/tmp\/.ICE-unix\"] --- @ --- --- You can also compute multiple things in a single pass over the stream: --- --- > Prelude Turtle> fold (select [1..10]) ((,) <$> Fold.minimum <*> Fold.maximum) --- > (Just 1,Just 10) --- --- If you are interested in this feature, check out the documentation in --- "Control.Foldl". - --- $io --- --- @turtle@ comes with built-in support for the standard text streams. --- --- For example, you can write to standard output using the `stdout` utility: --- --- @ --- `stdout` :: Shell Text -> IO () --- `stdout` s = sh (do --- txt <- s --- liftIO (echo txt) ) --- @ --- --- `stdout` outputs each `Text` value on its own line: --- --- > Prelude Turtle> stdout "Line 1" --- > Line 1 --- > Prelude Turtle> stdout ("Line 1" <|> "Line 2") --- > Line 1 --- > Line 2 --- --- Another useful stream is `stdin`, which emits one line of `Text` per line of --- standard input: --- --- @ --- `stdin` :: Shell Text --- @ --- --- Let's combine `stdin` and `stdout` to forward all input from standard input --- to standard output: --- --- > #!/usr/bin/env runhaskell --- > -- #!/bin/bash --- > {-# LANGUAGE OverloadedStrings #-} -- --- > -- --- > import Turtle -- --- > -- --- > main = stdout stdin -- cat --- --- If you run that it will continue to echo lines until you signal end of input --- using @Ctrl-D@: --- --- > $ ./example.hs --- > ABC<Enter> --- > ABC --- > Test<Enter> --- > Test --- > 42<Enter> --- > 42 --- > <Ctrl-D> --- > $ --- --- You can also read and write to files using the `input` and `output` --- utilities: --- --- @ --- Prelude Turtle> `output` \"file.txt\" (\"Test\" \<|\> \"ABC\" \<|\> \"42\") --- Prelude Turtle> stdout (`input` \"file.txt\") --- Test --- ABC --- 42 --- @ - --- $external --- --- You can embed external shell commands as streams within your Haskell program. --- --- For example, suppose that we want to use the system's built in @ls@ command. --- We can just run: --- --- > Prelude Turtle> stdout (inshell "ls" empty) --- > .X11-unix --- > .X0-lock --- > pulse-PKdhtXMmr18n --- > pulse-xHYcZ3zmN3Fv --- > tracker-gabriel --- > pulse-PYi1hSlWgNj2 --- > orbit-gabriel --- > ssh-vREYGbWGpiCa --- > .ICE-unix --- --- This works because type of `inshell` is: --- --- @ --- `inshell` --- :: Text -- Command line --- -> Shell Text -- Standard input to feed to program --- -> Shell Text -- Standard output produced by program --- @ --- --- This means you can use `inshell` to embed arbitrary external utilities as --- first class streams within your Haskell program: --- --- > Turtle Prelude> stdout (inshell "awk '{ print $1 }'" "123 456") --- > 123 --- --- You should also check out the `inproc` command, which is less powerful but --- safer since it decreases the likelihood of code injection or malformed --- commands: --- --- @ --- `inproc` --- :: Text -- Program --- -> [Text] -- Arguments --- -> Shell Text -- Standard input to feed to program --- -> Shell Text -- Standard output produced by program --- @ --- --- Using `inproc`, you would write: --- --- > Turtle Prelude> stdout (inproc "awk" ["{ print $1 }"] "123 456") --- > 123 - --- $patterns --- --- You can transform streams using Unix-like utilities. For example, you can --- filter a stream using `grep`. --- --- @ --- Prelude Turtle> stdout (input \"file.txt\") --- Test --- ABC --- 42 --- Prelude Turtle> stdout (`grep` \"ABC\" (input \"file.txt\")) --- ABC --- @ --- --- Let's look at the type of `grep`: --- --- @ --- `grep` :: Pattern a -> Shell Text -> Shell Text --- @ --- --- The first argument of `grep` is actually a `Pattern`, which implements --- `IsString`. When we pass a string literal we just create a `Pattern` that --- matches the given literal. --- --- `Pattern`s generalize regular expressions and you can use this table to --- roughly translate several regular expression idioms to `Pattern`s: --- --- @ --- Regex Pattern --- ========= ========= --- \"string\" \"string\" --- . `dot` --- e1 e2 e1 `<>` e2 --- e1 | e2 e1 `<|>` e2 --- e* `star` e --- e+ `plus` e --- e*? `selfless` (`star` e) --- e+? `selfless` (`plus` e) --- e{n} `count` n e --- e{m,n} `bounded` m n e --- e{0,n} `upperBounded` n e --- e? `optional` e --- [xyz] `oneOf` \"xyz\" --- [^xyz] `noneOf` \"xyz\" --- @ --- --- Here are some examples: --- --- > Prelude Turtle> -- grep '^[[:digit:]]\+$' file.txt --- > Prelude Turtle> stdout (grep (plus digit) (input "file.txt")) --- > 42 --- > Prelude Turtle> -- grep '^[[:digit:]]\+\|Test$' file.txt --- > Prelude Turtle> stdout (grep (plus digit <|> "Test") (input "file.txt")) --- > Test --- > 42 --- --- Note that @turtle@'s `grep` subtly differs from the traditional @grep@ --- command. The `Pattern` you provide must match the entire line. If you --- want to match the interior of a line, you can use the `has` utility: --- --- @ --- Prelude Turtle> -- grep B file.txt --- Prelude Turtle> stdout (grep (`has` \"B\") (input \"file.txt\")) --- ABC --- @ --- --- You can also use `prefix` or `suffix` to match the beginning or end of a --- string, respectively: --- --- @ --- Prelude Turtle> -- grep '^A' file.txt --- Prelude Turtle> stdout (grep (`prefix` \"A\") (input \"file.txt\")) --- ABC --- Prelude Turtle> -- grep 'C$' file.txt --- Prelude Turtle> stdout (grep (`suffix` \"C\") (input \"file.txt\")) --- ABC --- @ --- --- `sed` also uses `Pattern`s, too, and is more flexible than Unix @sed@: --- --- @ --- Prelude Turtle> -- sed 's/C/D/g' file.txt --- Prelude Turtle> stdout (`sed` (\"C\" `*>` return \"D\") (input \"file.txt\")) --- Test --- ABD --- 42 --- Prelude Turtle> -- sed 's\/[[:digit:]]\/!\/g' file.txt --- Prelude Turtle> stdout (`sed` (digit `*>` return \"!\") (input \"file.txt\")) --- Test --- ABC --- !! --- Prelude Turtle> import qualified Data.Text as Text --- Prelude Turtle> -- rev file.txt --- Prelude Turtle> stdout (`sed` (`fmap` Text.reverse (plus dot)) (input \"file.txt\")) --- tseT --- CBA --- 24 --- Prelude Turtle> --- @ --- --- You can also use `Pattern`s by themselves to parse arbitrary text into more --- structured values: --- --- @ --- Prelude Turtle> let pair = do x <- `decimal`; \" \"; y <- `decimal`; return (x, y) --- Prelude Turtle> :type pair --- pair :: `Pattern` (Integer, Integer) --- Prelude Turtle> `match` pair \"123 456\" --- [(123,456)] --- Prelude Turtle> data Pet = Cat | Dog deriving (Show) --- Prelude Turtle> let pet = (\"cat\" *> return Cat) \<|\> (\"dog\" *> return Dog) :: `Pattern` Pet --- Prelude Turtle> `match` pet \"dog\" --- [Dog] --- Prelude Turtle> `match` (pet \``sepBy`\` \",\") \"cat,dog,cat\" --- [[Cat,Dog,Cat]] --- @ --- --- See the "Turtle.Pattern" module for more details if you are interested in --- writing more complex `Pattern`s. - --- $exceptions --- --- Sometimes you may want to acquire resources and ensure they get released --- correctly if there are any exceptions. You can use `Managed` resources to --- acquire things safely within a `Shell`. --- --- You can think of a `Managed` resource as some resource that needs to be --- acquired and then released afterwards. Example: you want to create a --- temporary file and then guarantee it's deleted afterwards, even if the --- program fails with an exception. --- --- "Turtle.Prelude" provides two `Managed` utilities for creating temporary --- directories or files: --- --- @ --- `mktempdir` --- :: FilePath -- Parent directory --- -> Text -- Directory name template --- -> `Managed` FilePath -- Temporary directory --- @ --- --- @ --- `mktemp` --- :: FilePath -- Parent directory --- -> Text -- File name template --- -> `Managed` (FilePath, Handle) -- Temporary file --- @ --- --- You can acquire a `Managed` resource within a `Shell` with `using`: --- --- @ --- `using` :: Managed a -> Shell a --- @ --- --- ... and here is an example of creating a temporary directory and file within --- a `Shell`: --- --- > #!/usr/bin/env runhaskell --- > --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > --- > main = sh (do --- > dir <- using (mktempdir "/tmp" "turtle") --- > (file, _) <- using (mktemp dir "turtle") --- > liftIO (print file) ) --- --- When you run the above script it will print out the name of the temporary --- directory and file: --- --- > $ ./example.hs --- > FilePath "/tmp/turtle15976/turtle15976" --- --- ... and you can verify that they were deleted afterwards: --- --- > Turtle Prelude> view (find (has "turtle") "/tmp") --- > Turtle Prelude> -- No results --- --- As an exercise, try inserting an exception and verifying that the temporary: --- file and directory are still cleaned up correctly: --- --- > #!/usr/bin/env runhaskell --- > --- > {-# LANGUAGE OverloadedStrings #-} --- > --- > import Turtle --- > --- > main = sh (do --- > dir <- using (mktempdir "/tmp" "turtle") --- > (file, _) <- using (mktemp dir "turtle") --- > liftIO (print file) --- > liftIO (die "Urk!") ) --- --- To learn more about `Managed` resources, read the documentation in --- "Control.Monad.Managed". - --- $monadio --- --- If you are sick of having type `liftIO` everywhere, you can omit it. This --- is because all subroutines in @turtle@ are overloaded using the `MonadIO` --- type class, like our original `pwd` command where we first encountered the --- the `MonadIO` type: --- --- @ --- Prelude Turtle> :type pwd --- pwd :: `MonadIO` io => io Turtle.`Turtle.FilePath` --- @ --- --- This means you can this command is overloaded to run in any context that --- implements the `MonadIO` interface, including: --- --- * `IO` (obviously) --- --- * `Shell` --- --- * `Managed` --- --- You can tell if a type constructor like `Shell` implements `MonadIO` by --- clicking the link to the type constructor and looking for the instance list. --- There you will see a list of instances like: --- --- > Monad Shell --- > Functor Shell --- > MonadPlus Shell --- > Applicative Shell --- > Alternative Shell --- > MonadIO Shell --- > ... --- --- These instances represent the overloaded functions associated with `Shell` --- and we can see from the list that `Shell` implements `MonadIO` so we can --- use `pwd` (or any other subroutine in this library) within a `Shell`. --- --- However, not all subroutines in the Haskell ecosystem are overloaded in this --- 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 --- you would like to learn more advanced tricks, take the time to read the --- documentation in these modules: --- --- * "Turtle.Prelude" --- --- * "Turtle.Format" --- --- * "Turtle.Pattern" --- --- * "Turtle.Shell" --- --- * "Control.Foldl" --- --- * "Control.Monad.Managed" --- --- If you have more questions or need help learning the library, ask a question --- on Stack Overflow under the @haskell-turtle@ tag. For bugs or feature --- requests, create an issue on Github at --- <https://github.com/Gabriel439/Haskell-Turtle-Library/issues> --- --- 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! +{-# OPTIONS_GHC -fno-warn-unused-imports #-}++{-| Use @turtle@ if you want to write light-weight and maintainable shell+ scripts.++ @turtle@ embeds shell scripting directly within Haskell for three main+ reasons:++ * Haskell code is easy to refactor and maintain because the language is+ statically typed++ * Haskell is syntactically lightweight, thanks to global type inference++ * Haskell programs can be type-checked and interpreted very rapidly (< 1+ second)++ These features make Haskell ideal for scripting, particularly for replacing+ large and unwieldy Bash scripts.++ This tutorial introduces how to use the @turtle@ library to write Haskell+ scripts. This assumes no prior knowledge of Haskell, but does assume prior+ knowledge of Bash or a similar shell scripting language.++ If you are already proficient with Haskell, then you can get quickly up to+ speed by reading the Quick Start guide at the top of "Turtle.Prelude".++ The easiest way to follow along with the examples is to download the+ `stack` package management tool by following the instructions here:++ <https://github.com/commercialhaskell/stack>++ ... and then run:++> $ stack install turtle++-}++module Turtle.Tutorial (+ -- * Introduction+ -- $introduction++ -- * Comparison+ -- $compare++ -- * Subroutines+ -- $do++ -- * Types+ -- $types++ -- * Shell+ -- $shell++ -- * Type signatures+ -- $signatures++ -- * System+ -- $system++ -- * String formatting+ -- $format++ -- * Streams+ -- $streams++ -- * Loops+ -- $loops++ -- * Folds+ -- $folds++ -- * Input and output+ -- $io++ -- * External commands+ -- $external++ -- * Patterns+ -- $patterns++ -- * Exception Safety+ -- $exceptions++ -- * MonadIO+ -- $monadio++ -- * Command line options+ -- $cmdline++ -- * Conclusion+ -- $conclusion+ ) where++import Turtle++-- $introduction+-- Let's translate some simple Bash scripts to Haskell and work our way up to+-- more complex scripts. Here is an example \"Hello, world!\" script written+-- in both languages:+--+-- @+-- #!\/usr\/bin\/env stack+-- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- \ +-- -- #!\/bin\/bash+-- {-\# LANGUAGE OverloadedStrings \#-} --+-- --+-- import "Turtle" --+-- --+-- main = `echo` \"Hello, world!\" -- echo Hello, world!+-- @+--+-- In Haskell you can use @--@ to comment out the rest of a line. The above+-- example uses comments to show the equivalent Bash script side-by-side with+-- the Haskell script.+--+-- You can execute the above code by saving it to the file @example.hs@. If you+-- are copying and pasting the code, then remove the leading 1-space indent.+-- After you save the file, make the script executable and run the script:+--+-- > $ chmod u+x example.hs +-- > $ ./example.hs+-- > Hello, world!+--+-- If you delete the first two lines of the program, you can also compile the+-- above code to generate a native executable which will have a much faster+-- startup time and improved performance:+--+-- > $ # `-O2` turns on all optimizations+-- > $ # `-threaded` helps with piping shell output in and out of Haskell+-- > $ stack ghc -- -O2 -threaded example.hs+-- > $ ./example+-- > Hello, world!+--+-- You can even run Haskell code interactively using @ghci@, which is an+-- interactive REPL for Haskell. You can either use @ghci@ by itself:+--+-- > $ stack ghci+-- > <ghci links in some libraries>+-- > Prelude> :set -XOverloadedStrings+-- > Prelude> import Turtle+-- > Prelude Turtle> echo "Hello, world!"+-- > <ghci links in some libraries>+-- > Hello, world!+-- > Prelude Turtle> :quit+-- > $+--+-- From now on I'll omit @ghci@'s linker output in tutorial examples. You can+-- also silence this linker output by passing @--ghc-options -v0@ to+-- @stack ghci@.++-- $compare+-- You'll already notice a few differences between the Haskell code and Bash+-- code.+--+-- First, the Haskell code requires two additional lines of overhead to import+-- the @turtle@ library and enable overloading of string literals. This+-- overhead is mostly unavoidable.+--+-- Second, the Haskell `echo` explicitly quotes its string argument whereas the+-- Bash @echo@ does not. In Bash every token is a string by default and you+-- distinguish variables by prepending a dollar sign to them. In Haskell+-- the situation is reversed: every token is a variable by default and you+-- distinguish strings by quoting them. The following example highlights the+-- difference:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- >+-- > -- #!/bin/bash+-- > {-# LANGUAGE OverloadedStrings #-} --+-- > --+-- > import Turtle --+-- > --+-- > str = "Hello!" --STR=Hello!+-- > --+-- > main = echo str --echo $STR+--+-- Third, you have to explicitly assign a subroutine to @main@ to specify which+-- subroutine to run when your program begins. This is because Haskell lets you+-- define things out of order. For example, we could have written our original+-- program this way instead:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > main = echo str+-- > +-- > str = "Hello, world!"+--+-- Notice how the above program defines @str@ after @main@, which is valid.+-- Haskell does not care in what order you define top-level values or functions+-- (using the @=@ sign). However, the top level of a Haskell program only+-- permits definitions. If you were to insert a statement at the top-level:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > echo "Hello, world!"+--+-- ... then you would get this error when you tried to run your program:+--+-- > example.hs:7:1: Parse error: naked expression at top level++-- $do+-- You can use @do@ notation to create a subroutine that runs more than one+-- command:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- >+-- > -- #!/bin/bash+-- > {-# LANGUAGE OverloadedStrings #-} --+-- > --+-- > import Turtle --+-- > --+-- > main = do --+-- > echo "Line 1" -- echo Line 1+-- > echo "Line 2" -- echo Line 2+-- +-- > $ ./example.hs+-- > Line 1+-- > Line 2+--+-- @do@ blocks can use either use the indentation level to control their+-- duration or they can use curly braces and semicolons. To see the full rules+-- for @do@ syntax, read: <http://en.wikibooks.org/wiki/Haskell/Indentation>.+--+-- Some commands can return a value, and you can store the result of a command+-- using the @<-@ symbol. For example, the following program prints the+-- creation time of the current working directory by storing two intermediate+-- results:+--+-- @+-- #!\/usr\/bin\/env stack+-- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- \ +-- -- #!\/bin\/bash+-- import Turtle --+-- --+-- main = do --+-- dir <- `pwd` -- DIR=$(pwd)+-- time <- `datefile` dir -- TIME=$(date -r $DIR)+-- `print` time -- echo $TIME+-- @+--+-- > $ ./example.hs+-- > 2015-01-24 03:40:31 UTC+--+-- The main difference between @=@ and @<-@ is that:+--+-- * The @<-@ symbol is overloaded and its meaning is context-dependent; in this+-- context it just means \"store the current result\"+--+-- * The @=@ symbol is not overloaded and always means that the two sides of the+-- equality are interchangeable+--+-- @do@ notation lets you combine smaller subroutines into larger subroutines.+-- For example, we could refactor the above code to split the first two commands+-- into their own smaller subroutine and then invoke that smaller subroutine+-- within a larger subroutine:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- >+-- > -- #!/bin/bash+-- > import Turtle --+-- > --+-- > datePwd = do -- datePwd() {+-- > dir <- pwd -- DIR=$(pwd)+-- > result <- datefile dir -- RESULT=$(date -r $DIR)+-- > return result -- echo $RESULT+-- > -- }+-- > main = do --+-- > time <- datePwd -- TIME=$(datePwd)+-- > print time -- echo $TIME+--+-- The refactored program still returns the exact same result:+--+-- > $ ./example.hs+-- > 2015-01-24 03:40:31 UTC+--+-- We can also simplify the code a little bit because @do@ notation implicitly+-- returns the value of the last command within a subroutine. We can use this+-- trick to simplify both the Haskell and Bash code:+--+-- > datePwd = do -- datePwd() {+-- > dir <- pwd -- DIR=$(pwd)+-- > datefile dir -- date -r $DIR+-- > -- }+--+-- However, keep in mind that the `return` statement is something of a misnomer+-- since it does not break or exit from the surrounding subroutine. All it+-- does is create a trivial subroutine that has no side effects and returns its+-- argument as its result. If you `return` an expression, you're just giving+-- it a new name:+--+-- > do x <- return expr -- X=EXPR+-- > command x -- command $X+-- > +-- > -- Same as:+-- > command expr -- command EXPR+--+-- In fact, the first line is equivalent to @let x = expr@, which more closely+-- mirrors the equivalent Bash syntax:+--+-- > do let x = expr -- X=EXPR+-- > command x -- command $X+-- > +-- > -- Same as:+-- > command expr -- command EXPR+--+-- Also, for a subroutine with a single command, you can omit the @do@:+--+-- > main = do echo "Hello, world!"+-- > +-- > -- Same as:+-- > main = echo "Hello, world!"++-- $types+--+-- Notice how the above Haskell example used `print` instead of `echo`. Run the+-- following script to find out what happens if we choose `echo` instead:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > import Turtle+-- > +-- > main = do+-- > dir <- pwd+-- > time <- datefile dir+-- > echo time+--+-- If we run that we get a type error:+--+-- > $ ./example.hs+-- > +-- > example.hs:8:10:+-- > Couldn't match expected type `Text' with actual type `UTCTime'+-- > In the first argument of `echo', namely `time'+-- > In a stmt of a 'do' block: echo time+-- > In the expression:+-- > do { dir <- pwd;+-- > time <- datefile dir;+-- > echo time }+--+-- The error points to the last line of our program: @(example.hs:8:10)@ means+-- line 8, column 10 of our program. If you study the error message closely+-- you'll see that the `echo` function expects a `Text` value, but we passed it+-- @\'time\'@, which was a `UTCTime` value. Although the error is at the end of+-- our script, Haskell catches this error before even running the script. When+-- we \"interpret\" a Haskell script the Haskell compiler actually compiles the+-- script without any optimizations to generate a temporary executable and then+-- runs the executable, much like Perl does for Perl scripts.+--+-- You might wonder: \"where are the types?\" None of the above programs had+-- any type signatures or type annotations, yet the compiler still detected type+-- errors correctly. This is because Haskell uses \"global type inference\" to+-- detect errors, meaning that the compiler can infer the types of expressions+-- within the program without any assistance from the programmer.+--+-- You can even ask the compiler what the type of an expression is using @ghci@.+-- Let's open up the REPL and import this library so that we can study the types+-- and deduce why our program failed:+--+-- > $ stack ghci+-- > Prelude> import Turtle+-- > Prelude Turtle>+--+-- You can interrogate the REPL for an expression's type using the @:type@+-- command:+--+-- @+-- Prelude Turtle> :type pwd+-- pwd :: `MonadIO` io => io Turtle.`Turtle.FilePath`+-- @+--+-- For right now, ignore all occurrences of `MonadIO` and just read the type+-- as:+--+-- @+-- Prelude Turtle> :type pwd+-- pwd :: `IO` Turtle.`Turtle.FilePath`+-- @+--+-- We will cover `MonadIO` later on.+--+-- Whenever you see something of the form @(x :: t)@, that means that @\'x\'@+-- is a value of type @\'t\'@. The REPL says that `pwd` is a subroutine ('IO')+-- that returns a `Turtle.FilePath`. The "Turtle" prefix before+-- `Turtle.FilePath` is just the module name since the `Turtle.FilePath`+-- exported by the @turtle@ library conflicts with the default `FilePath`+-- exported by Haskell's @Prelude@. The compiler uses the fully qualified name,+-- @"Turtle".`FilePath`@, to avoid ambiguity.+--+-- We can similarly ask for the type of `datefile`:+--+-- @+-- Prelude Turtle> :type datefile+-- datefile :: Turtle.`Turtle.FilePath` -> `IO` `UTCTime`+-- @+--+-- `datefile` is a function whose argument must be a `Turtle.FilePath` and whose+-- result is a subroutine (`IO`) that returns a `UTCTime`. Notice how the+-- input argument of `datefile` (which is a `Turtle.FilePath`) is the same type+-- as the return value of `pwd` (also a `Turtle.FilePath`).+--+-- Now let's study type of `echo` to see why we get the type error:+--+-- @+-- Prelude Turtle> :type echo+-- echo :: `Text` -> `IO` ()+-- @+--+-- The above type says that `echo` is a function whose argument is a value of+-- type `Text` and whose result is a subroutine (`IO`) with an empty return+-- value (denoted @\'()\'@).+--+-- Now we can understand the type error: `echo` expects a `Text` argument but+-- `datefile` returns a `UTCTime`, which is not the same thing. Unlike Bash,+-- not everything is `Text` in Haskell and the compiler will not cast or coerce+-- types for you.+--+-- The reason `print` worked is because `print` has a more general type than+-- `echo`:+--+-- @+-- Prelude Turtle> :type print+-- print :: `Show` a => a -> `IO` ()+-- @+--+-- This type signature says that `print` can display any value of type @\'a\'@+-- so long as @\'a\'@ implements the `Show` interface. In this case `UTCTime`+-- does implement the `Show` interface, so everything works out when we use+-- `print`.+--+-- This library provides a helper function that lets you convert any type that+-- implements `Show` into a `Text` value:+-- +-- @+-- \-\- This behaves like Python's \`repr\` function+-- `repr` :: `Show` a => a -> `Text`+-- @+--+-- You could therefore implement `print` in terms of `echo` and `repr`:+--+-- > print x = echo (repr x)++-- $shell+--+-- You can use @ghci@ for more than just inferring types. @ghci@ is a+-- general-purpose Haskell shell for your system when you extend it with+-- @turtle@:+--+-- @+-- $ stack ghci+-- Prelude> :set -XOverloadedStrings+-- Prelude> import Turtle+-- Prelude Turtle> `cd` \"/tmp\"+-- Prelude Turtle> `pwd`+-- FilePath \"/tmp\"+-- Prelude Turtle> `mkdir` \"test\"+-- Prelude Turtle> `cd` \"test\"+-- Prelude Turtle> `touch` \"file\"+-- Prelude Turtle> `testfile` \"file\"+-- True+-- Prelude Turtle> `rm` \"file\"+-- Prelude Turtle> `testfile` \"file\"+-- False+-- @+--+-- You can also optionally configure @ghci@ to run the first two commands every+-- time you launch @ghci@. Just create a @.ghci@ within your current directory+-- with these two lines:+--+-- > :set -XOverloadedStrings+-- > import Turtle+--+-- The following @ghci@ examples will all assume that you run these two commands+-- at the beginning of every session, either manually or automatically. You can+-- even enable those two commands permanently by adding the above @.ghci@ file+-- to your home directory.+--+-- Within @ghci@ you can run a subroutine and @ghci@ will `print` the+-- subroutine's value if it is not empty:+--+-- @+-- Prelude Turtle> `shell` \"true\" empty+-- ExitSuccess+-- Prelude Turtle> `shell` \"false\" empty+-- ExitFailure 1+-- @+--+-- You can also type in a pure expression and @ghci@ will evaluate that+-- expression:+--+-- @+-- Prelude Turtle> 2 + 2+-- 4+-- Prelude Turtle> \"123\" `<>` \"456\" -- (\<\>) concatenates strings+-- \"123456\"+-- @+--+-- This works because @ghci@ automatically wraps anything that's not a+-- subroutine with `print`. It's as if we had written:+--+-- > Prelude Turtle> print (2 + 2)+-- > 4+-- > Prelude Turtle> print ("123" <> "456")+-- > "123456"++-- $signatures+--+-- Haskell performs global type inference, meaning that the compiler never+-- requires any type signatures. When you add type signatures, they are purely+-- for the benefit of the programmer and behave like machine-checked+-- documentation.+--+-- Let's illustrate this by adding types to our original script:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > import Turtle+-- > +-- > datePwd :: IO UTCTime -- Type signature+-- > datePwd = do+-- > dir <- pwd+-- > datefile dir+-- > +-- > main :: IO () -- Type signature+-- > main = do+-- > time <- datePwd+-- > print time+--+-- The first type signature says that @datePwd@ is a subroutine that returns a+-- `UTCTime`:+--+-- > -- +----- A subroutine ...+-- > -- |+-- > -- | +-- ... that returns `UTCTime`+-- > -- | |+-- > -- v v+-- > datePwd :: IO UTCTime+--+-- The second type signature says that @main@ is a subroutine that returns an+-- empty value:+--+-- > -- +----- A subroutine ...+-- > -- |+-- > -- | +-- ... that returns an empty value (i.e. `()`)+-- > -- | |+-- > -- v v+-- > main :: IO ()+--+-- Not every top-level value has to be a subroutine, though. For example, you+-- can define unadorned `Text` values at the top-level, as we saw previously:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > str :: Text+-- > str = "Hello!"+-- > +-- > main :: IO ()+-- > main = echo str+--+-- These type annotations do not assist the compiler. Instead, the compiler+-- independently infers the type and then checks whether it matches the+-- documented type. If there is a mismatch the compiler will raise a type+-- error.+--+-- Let's test this out by providing an incorrect type for @\'str\'@:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > str :: Int+-- > str = "Hello!"+-- > +-- > main :: IO ()+-- > main = echo str+--+-- If you run that script, you will get two error messages:+--+-- > $ ./example.hs+-- > +-- > example.hs:8:7:+-- > No instance for (IsString Int)+-- > arising from the literal `"Hello, world!"'+-- > Possible fix: add an instance declaration for (IsString Int)+-- > In the expression: "Hello, world!"+-- > In an equation for `str': str = "Hello, world!"+-- > +-- > example.hs:11:13:+-- > Couldn't match expected type `Text' with actual type `Int'+-- > In the first argument of `echo', namely `str'+-- > In the expression: echo str+-- > In an equation for `main': main = echo str+--+-- The first error message relates to the @OverloadedStrings@ extensions. When+-- we enable @OverloadedStrings@ the compiler overloads string literals,+-- interpreting them as any type that implements the `IsString` interface. The+-- error message says that `Int` does not implement the `IsString` interface so+-- the compiler cannot interpret a string literal as an `Int`. On the other+-- hand the `Text` and `Turtle.FilePath` types do implement `IsString`, which+-- is why we can interpret string literals as `Text` or `Turtle.FilePath`+-- values.+--+-- The second error message says that `echo` expects a `Text` value, but we+-- declared @str@ to be an `Int`, so the compiler aborts compilation, requiring+-- us to either fix or delete our type signature.+--+-- Notice that there is nothing wrong with the program other than the type+-- signature we added. If we were to delete the type signature the program+-- would compile and run correctly. The sole purpose of this type signature is+-- for us to communicate our expectations to the compiler so that the compiler+-- can alert us if the code does not match our expectations.+--+-- Let's also try reversing the type error, providing a number where we expect+-- a string:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > str :: Text+-- > str = 4+-- > +-- > main :: IO ()+-- > main = echo str+--+-- This gives a different error:+--+-- > $ ./example.hs+-- > +-- > example.hs:8:7:+-- > No instance for (Num Text)+-- > arising from the literal `4'+-- > Possible fix: add an instance declaration for (Num Text)+-- > In the expression: 4+-- > In an equation for `str': str = 4+--+-- Haskell also automatically overloads numeric literals, too. The compiler+-- interprets integer literals as any type that implements the `Num` interface.+-- The `Text` type does not implement the `Num` interface, so we cannot+-- interpret integer literals as `Text` strings.++-- $system+--+-- You can invoke arbitrary shell commands using the `shell` command. For+-- example, we can write a program that creates an empty directory and then+-- uses a `shell` command to archive the directory:+--+-- @+-- #!\/usr\/bin\/env stack+-- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- \ +-- -- #!\/bin\/bash+-- {-\# LANGUAGE OverloadedStrings \#-} --+-- --+-- import Turtle --+-- --+-- main = do --+-- mkdir \"test\" -- mkdir test+-- `shell` \"tar czf test.tar.gz test\" empty -- tar czf test.tar.gz test+-- @+--+-- If you run this program, it will generate the @test.tar.gz@ archive:+--+-- > $ ./example.hs+-- > ExitSuccess+-- > $ echo $?+-- > 0+-- > $ ls test.tar.gz+-- > test.tar.gz+--+-- Like @ghci@, the @runhaskell@ command running our script prints any non-empty+-- result of the @main@ subroutine (`ExitSuccess` in this case).+--+-- The easiest way to learn a new command like `shell` is to view its+-- documentation. Click on the word `shell`, which will take you to+-- documentation that looks like this:+--+-- @+-- `shell`+-- :: Text -- Command line+-- -> Shell Text -- Standard input (as lines of \`Text\`)+-- -> IO `ExitCode` -- Exit code of the shell command+-- @+--+-- The first argument is a `Text` representation of the command to run. The+-- second argument lets you feed input to the command, and you can provide+-- `empty` for now to feed no input.+--+-- The final result is an `ExitCode`, which you can use to detect whether the+-- command completed successfully. For example, we could print a more+-- descriptive error message if an external command fails:+--+-- @+-- #!\/usr\/bin\/env stack+-- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- +-- {-\# LANGUAGE OverloadedStrings \#-}+-- +-- import Turtle+--+-- main = do+-- let cmd = \"false\"+-- x <- shell cmd empty+-- case x of+-- ExitSuccess -> return ()+-- ExitFailure n -> `die` (cmd \<\> \" failed with exit code: \" \<\> repr n)+-- @+--+-- This prints an error message since the @false@ command always fails:+--+-- > $ ./example.hs+-- > example.hs: user error (false failed with exit code: 1)+--+-- You should also check out the `proc` command, which is less powerful but+-- safer since it decreases the likelihood of code injection or malformed+-- commands:+--+-- @+-- `proc`+-- :: Text -- Program+-- :: [Text] -- Arguments+-- -> Shell Text -- Standard input (as lines of \`Text\`)+-- -> IO ExitCode -- Exit code of the shell command+-- @+--+-- Most of the commands in this library do not actually invoke an external+-- shell or program. Instead, they indirectly wrap other Haskell libraries that+-- bind to C code.++-- $format+--+-- This library provides type-safe string formatting utilities, too. For+-- example, instead of writing this:+--+-- > cmd <> " failed with exit code: " <> repr n+--+-- ... you could format the string using @printf@ style instead:+--+-- > format (s%" failed with exit code: "%d) cmd n+--+-- What's neat is that the compiler will automatically infer the number of+-- arguments and their types from the `Format` string:+--+-- > $ stack ghci+-- > Prelude Turtle> :type format (s%" failed with exit code: "%d)+-- > format (s%" failed with exit code: "%d) :: Text -> Int -> Text+--+-- The compiler deduces that the above `Format` string requires one argument of+-- type `Text` to satisfy the `s` at the beginning of the format string and+-- another argument of type `Int` to satisfy the `d` at the end of the format+-- string.+--+-- If you are interested in this feature, check out the "Turtle.Format" module+-- for more details.++-- $streams+-- The @turtle@ library provides support for streaming computations, just like+-- Bash. The primitive @turtle@ streams are little more verbose than their+-- Bash counterparts, but @turtle@ streams can be built and combined in more+-- ways.+--+-- The key type for streams is the `Shell` type, which represents a stream of+-- values. For example, the `ls` function has a streaming result:+--+-- @+-- Prelude Turtle> :type `ls`+-- `ls` :: Turtle.FilePath -> `Shell` Turtle.FilePath+-- @+--+-- That type says that `ls` takes a single `Turtle.FilePath` as its argument+-- (the directory to list) and the result is a `Shell` stream of+-- `Turtle.FilePath`s (the immediate children of that directory).+--+-- You can't run a `Shell` stream directly within @ghci@. You will get a type+-- error like this if you try:+--+-- > Prelude Turtle> ls "/tmp"+-- > +-- > <interactive>:2:1:+-- > No instance for (Show (Shell Turtle.FilePath))+-- > arising from a use of `print'+-- > Possible fix:+-- > add an instance declaration for (Show (Shell Turtle.FilePath))+-- > In a stmt of an interactive GHCi command: print it+--+-- Instead, you must consume the stream as it is generated and the simplest way+-- to consume a `Shell` stream is `view`:+--+-- @+-- `view` :: Show a => Shell a -> IO ()+-- @+--+-- `view` takes any `Shell` stream of values and `print`s them to standard+-- output:+--+-- > Prelude Turtle> view (ls "/tmp")+-- > FilePath "/tmp/.X11-unix"+-- > FilePath "/tmp/.X0-lock"+-- > FilePath "/tmp/pulse-PKdhtXMmr18n"+-- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv"+-- > FilePath "/tmp/tracker-gabriel"+-- > FilePath "/tmp/pulse-PYi1hSlWgNj2"+-- > FilePath "/tmp/orbit-gabriel"+-- > FilePath "/tmp/ssh-vREYGbWGpiCa"+-- > FilePath "/tmp/.ICE-unix+--+-- You can build your own `Shell` streams using a few primitive operations,+--+-- The first primitive is `empty`, which represents an empty stream of values:+--+-- @+-- Prelude Turtle> view `empty` -- Outputs nothing+-- Prelude Turtle>+-- @+--+-- Another way to say that is:+--+-- @+-- view `empty` = return ()+-- @+--+-- The type of empty is:+--+-- @+-- `empty` :: Shell a+-- @+-- +-- The lower-case @\'a\'@ is \"polymorphic\", meaning that it will type check as+-- any type. That means that you can produce an `empty` stream of any type of+-- value.+--+-- The next simplest function is `return`, which lets you take any value and+-- transform it into a singleton `Shell` that emits just that one value:+--+-- @+-- Prelude Turtle> view (`return` 1)+-- 1+-- @+--+-- Another way to say that is:+--+-- @+-- view (`return` x) = print x+-- @+--+-- The type of `return` is:+--+-- @+-- `return` :: a -> Shell a+-- @+--+-- Notice that this is the same `return` function we saw before. This is+-- because `return` is overloaded and works with both `IO` and `Shell`.+--+-- You can also take any subroutine ('IO') and transform it into a singleton+-- `Shell`:+--+-- @+-- Prelude Turtle> view (`liftIO` readline)+-- ABC\<Enter\>+-- Just \"ABC\"+-- @+--+-- Another way to say that is:+--+-- @+-- view (`liftIO` io) = do x <- io+-- print x+-- @+--+-- The type of `liftIO` is:+--+-- @+-- `liftIO` :: IO a -> Shell a+-- @+--+-- Once you have those primitive `Shell` streams you can begin to combine them+-- into larger `Shell` streams. For example, you can concatenate two `Shell`+-- streams using (`<|>`):+--+-- @+-- view (return 1 `<|>` return 2)+-- 1+-- 2+-- @+--+-- Another way to say that is:+--+-- @+-- view (xs `<|>` ys) = do view xs+-- view ys+-- @+--+-- The type of (`<|>`) is:+--+-- @+-- (`<|>`) :: Shell a -> Shell a -> Shell a+-- @+--+-- In other words, you can concatenate two `Shell` streams of the same element+-- type to get a new `Shell` stream, also of the same element type.+--+-- Let's try using (`<|>`) on two real streams:+--+-- > Prelude Turtle> view (ls "/tmp" <|> ls "/usr")+-- > FilePath "/tmp/.X11-unix"+-- > FilePath "/tmp/.X0-lock"+-- > FilePath "/tmp/pulse-PKdhtXMmr18n"+-- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv"+-- > FilePath "/tmp/tracker-gabriel"+-- > FilePath "/tmp/pulse-PYi1hSlWgNj2"+-- > FilePath "/tmp/orbit-gabriel"+-- > FilePath "/tmp/ssh-vREYGbWGpiCa"+-- > FilePath "/tmp/.ICE-unix"+-- > FilePath "/usr/lib"+-- > FilePath "/usr/src"+-- > FilePath "/usr/sbin"+-- > FilePath "/usr/include"+-- > FilePath "/usr/share"+-- > FilePath "/usr/games"+-- > FilePath "/usr/local"+-- > FilePath "/usr/bin"+--+-- Finally, note that `Shell` implements the `IsString` interface, so a string+-- literal will type-check as a `Shell` that emits a single `Text` value:+--+-- > Prelude Turtle> view "123"+-- > "123"+-- > Prelude Turtle> view (return "123") -- Same thing+-- > "123"+-- > Prelude Turtle> view ("123" <|> "456")+-- > "123"+-- > "456"+-- > Prelude Turtle> view (return "123" <|> return "456") -- Same thing+-- > "123"+-- > "456"++-- $loops+--+-- This library also provides the `select` function for conveniently emitting a+-- list of values:+--+-- @+-- Prelude Turtle> view (`select` [1, 2, 3])+-- 1+-- 2+-- 3+-- @+--+-- We can use `select` to implement loops within a `Shell`:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > -- #!/bin/bash+-- > {-# LANGUAGE OverloadedStrings #-} --+-- > --+-- > import Turtle --+-- > --+-- > example = do --+-- > x <- select [1, 2] -- for x in 1 2; do+-- > y <- select [3, 4] -- for y in 3 4; do+-- > liftIO (print (x, y)) -- echo \(${x},${y}\);+-- > -- done;+-- > main = sh example -- done+--+-- That will `print` every permutation of @\'x\'@ and @\'y\'@:+--+-- > $ ./example+-- > (1,3)+-- > (1,4)+-- > (2,3)+-- > (2,4)+--+-- This uses the `sh` utility instead of `view`. The only difference is that+-- `sh` doesn't print any values (since `print` is doing that already):+--+-- @+-- `sh` :: Shell a -> IO ()+-- @+--+-- This trick isn't limited to `select`. You can loop over the output of any+-- `Shell` by just binding its result. For example, this is how `view` loops+-- over its argument:+--+-- > view :: Show a => Shell a -> IO ()+-- > view s = sh (do+-- > x <- s -- `x` ranges over every output of `s`+-- > liftIO (print x) )+--+-- You can also loop over a stream in a one-liner, still using @do@ notation.+-- Just insert semi-colons between statements:+--+-- > Prelude Turtle> -- for file in /tmp/*; do echo $file; done+-- > Prelude Turtle> sh (do file <- ls "/tmp"; liftIO (print file))+-- > FilePath "/tmp/.X11-unix"+-- > FilePath "/tmp/.X0-lock"+-- > FilePath "/tmp/pulse-PKdhtXMmr18n"+-- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv"+-- > FilePath "/tmp/tracker-gabriel"+-- > FilePath "/tmp/pulse-PYi1hSlWgNj2"+-- > FilePath "/tmp/orbit-gabriel"+-- > FilePath "/tmp/ssh-vREYGbWGpiCa"+-- > FilePath "/tmp/.ICE-unix"++-- $folds+--+-- There are other ways you can consume a `Shell` stream. For example, you can+-- `fold` the stream using predefined `Fold`s from "Control.Foldl":+--+-- @+-- Prelude Turtle> import qualified "Control.Foldl" as Fold+-- Prelude Turtle Fold> `fold` (ls \"/tmp\") Fold.length+-- 9+-- @+--+-- @+-- Prelude Turtle Fold> `fold` (ls \"/tmp\") Fold.head+-- Just (FilePath \"\/tmp\/.X11-unix\")+-- @+--+-- @+-- Prelude Turtle Fold> `fold` (ls \"\/tmp\") Fold.list+-- [FilePath \"\/tmp\/.X11-unix\",FilePath \"\/tmp\/.X0-lock\",FilePath \"\/tmp\/pulse-PKd+-- htXMmr18n\",FilePath \"\/tmp\/pulse-xHYcZ3zmN3Fv\",FilePath \"\/tmp\/tracker-gabriel+-- \",FilePath \"\/tmp\/pulse-PYi1hSlWgNj2\",FilePath \"\/tmp\/orbit-gabriel\",FilePath +-- \"\/tmp\/ssh-vREYGbWGpiCa\",FilePath \"\/tmp\/.ICE-unix\"]+-- @+--+-- You can also compute multiple things in a single pass over the stream:+--+-- > Prelude Turtle> fold (select [1..10]) ((,) <$> Fold.minimum <*> Fold.maximum)+-- > (Just 1,Just 10)+--+-- If you are interested in this feature, check out the documentation in+-- "Control.Foldl".++-- $io+--+-- @turtle@ comes with built-in support for the standard text streams.+--+-- For example, you can write to standard output using the `stdout` utility:+--+-- @+-- `stdout` :: Shell Text -> IO ()+-- `stdout` s = sh (do+-- txt <- s+-- liftIO (echo txt) )+-- @+--+-- `stdout` outputs each `Text` value on its own line:+--+-- > Prelude Turtle> stdout "Line 1"+-- > Line 1+-- > Prelude Turtle> stdout ("Line 1" <|> "Line 2")+-- > Line 1+-- > Line 2+--+-- Another useful stream is `stdin`, which emits one line of `Text` per line of+-- standard input:+--+-- @+-- `stdin` :: Shell Text+-- @+--+-- Let's combine `stdin` and `stdout` to forward all input from standard input+-- to standard output:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > -- #!/bin/bash+-- > {-# LANGUAGE OverloadedStrings #-} --+-- > --+-- > import Turtle --+-- > --+-- > main = stdout stdin -- cat+--+-- If you run that it will continue to echo lines until you signal end of input+-- using @Ctrl-D@:+--+-- > $ ./example.hs+-- > ABC<Enter>+-- > ABC+-- > Test<Enter>+-- > Test+-- > 42<Enter>+-- > 42+-- > <Ctrl-D>+-- > $+--+-- You can also read and write to files using the `input` and `output`+-- utilities:+--+-- @+-- Prelude Turtle> `output` \"file.txt\" (\"Test\" \<|\> \"ABC\" \<|\> \"42\")+-- Prelude Turtle> stdout (`input` \"file.txt\")+-- Test+-- ABC+-- 42+-- @++-- $external+--+-- You can embed external shell commands as streams within your Haskell program.+--+-- For example, suppose that we want to use the system's built in @ls@ command.+-- We can just run:+--+-- > Prelude Turtle> stdout (inshell "ls" empty)+-- > .X11-unix+-- > .X0-lock+-- > pulse-PKdhtXMmr18n+-- > pulse-xHYcZ3zmN3Fv+-- > tracker-gabriel+-- > pulse-PYi1hSlWgNj2+-- > orbit-gabriel+-- > ssh-vREYGbWGpiCa+-- > .ICE-unix+--+-- This works because type of `inshell` is:+--+-- @+-- `inshell`+-- :: Text -- Command line+-- -> Shell Text -- Standard input to feed to program+-- -> Shell Text -- Standard output produced by program+-- @+--+-- This means you can use `inshell` to embed arbitrary external utilities as+-- first class streams within your Haskell program:+--+-- > Turtle Prelude> stdout (inshell "awk '{ print $1 }'" "123 456")+-- > 123+--+-- You should also check out the `inproc` command, which is less powerful but+-- safer since it decreases the likelihood of code injection or malformed+-- commands:+--+-- @+-- `inproc`+-- :: Text -- Program+-- -> [Text] -- Arguments+-- -> Shell Text -- Standard input to feed to program+-- -> Shell Text -- Standard output produced by program+-- @+--+-- Using `inproc`, you would write:+--+-- > Turtle Prelude> stdout (inproc "awk" ["{ print $1 }"] "123 456")+-- > 123++-- $patterns+--+-- You can transform streams using Unix-like utilities. For example, you can+-- filter a stream using `grep`.+--+-- @+-- Prelude Turtle> stdout (input \"file.txt\")+-- Test+-- ABC+-- 42+-- Prelude Turtle> stdout (`grep` \"ABC\" (input \"file.txt\"))+-- ABC+-- @+--+-- Let's look at the type of `grep`:+--+-- @+-- `grep` :: Pattern a -> Shell Text -> Shell Text+-- @+--+-- The first argument of `grep` is actually a `Pattern`, which implements+-- `IsString`. When we pass a string literal we just create a `Pattern` that+-- matches the given literal.+--+-- `Pattern`s generalize regular expressions and you can use this table to+-- roughly translate several regular expression idioms to `Pattern`s:+--+-- @+-- Regex Pattern+-- ========= =========+-- \"string\" \"string\"+-- . `dot`+-- e1 e2 e1 `<>` e2+-- e1 | e2 e1 `<|>` e2+-- e* `star` e+-- e+ `plus` e+-- e*? `selfless` (`star` e)+-- e+? `selfless` (`plus` e)+-- e{n} `count` n e+-- e{m,n} `bounded` m n e+-- e{0,n} `upperBounded` n e+-- e? `optional` e+-- [xyz] `oneOf` \"xyz\"+-- [^xyz] `noneOf` \"xyz\"+-- @+--+-- Here are some examples:+--+-- > Prelude Turtle> -- grep '^[[:digit:]]\+$' file.txt+-- > Prelude Turtle> stdout (grep (plus digit) (input "file.txt"))+-- > 42+-- > Prelude Turtle> -- grep '^[[:digit:]]\+\|Test$' file.txt+-- > Prelude Turtle> stdout (grep (plus digit <|> "Test") (input "file.txt"))+-- > Test+-- > 42+--+-- Note that @turtle@'s `grep` subtly differs from the traditional @grep@+-- command. The `Pattern` you provide must match the entire line. If you+-- want to match the interior of a line, you can use the `has` utility:+--+-- @+-- Prelude Turtle> -- grep B file.txt+-- Prelude Turtle> stdout (grep (`has` \"B\") (input \"file.txt\"))+-- ABC+-- @+--+-- You can also use `prefix` or `suffix` to match the beginning or end of a+-- string, respectively:+--+-- @+-- Prelude Turtle> -- grep '^A' file.txt+-- Prelude Turtle> stdout (grep (`prefix` \"A\") (input \"file.txt\"))+-- ABC+-- Prelude Turtle> -- grep 'C$' file.txt+-- Prelude Turtle> stdout (grep (`suffix` \"C\") (input \"file.txt\"))+-- ABC+-- @+--+-- `sed` also uses `Pattern`s, too, and is more flexible than Unix @sed@:+--+-- @+-- Prelude Turtle> -- sed 's/C/D/g' file.txt+-- Prelude Turtle> stdout (`sed` (\"C\" `*>` return \"D\") (input \"file.txt\"))+-- Test+-- ABD+-- 42+-- Prelude Turtle> -- sed 's\/[[:digit:]]\/!\/g' file.txt+-- Prelude Turtle> stdout (`sed` (digit `*>` return \"!\") (input \"file.txt\"))+-- Test+-- ABC+-- !!+-- Prelude Turtle> import qualified Data.Text as Text+-- Prelude Turtle> -- rev file.txt+-- Prelude Turtle> stdout (`sed` (`fmap` Text.reverse (plus dot)) (input \"file.txt\"))+-- tseT+-- CBA+-- 24+-- Prelude Turtle>+-- @+--+-- You can also use `Pattern`s by themselves to parse arbitrary text into more+-- structured values:+--+-- @+-- Prelude Turtle> let pair = do x <- `decimal`; \" \"; y <- `decimal`; return (x, y)+-- Prelude Turtle> :type pair+-- pair :: `Pattern` (Integer, Integer)+-- Prelude Turtle> `match` pair \"123 456\"+-- [(123,456)]+-- Prelude Turtle> data Pet = Cat | Dog deriving (Show)+-- Prelude Turtle> let pet = (\"cat\" *> return Cat) \<|\> (\"dog\" *> return Dog) :: `Pattern` Pet+-- Prelude Turtle> `match` pet \"dog\"+-- [Dog]+-- Prelude Turtle> `match` (pet \``sepBy`\` \",\") \"cat,dog,cat\"+-- [[Cat,Dog,Cat]]+-- @+--+-- See the "Turtle.Pattern" module for more details if you are interested in+-- writing more complex `Pattern`s.++-- $exceptions+--+-- Sometimes you may want to acquire resources and ensure they get released+-- correctly if there are any exceptions. You can use `Managed` resources to+-- acquire things safely within a `Shell`.+--+-- You can think of a `Managed` resource as some resource that needs to be+-- acquired and then released afterwards. Example: you want to create a+-- temporary file and then guarantee it's deleted afterwards, even if the+-- program fails with an exception.+--+-- "Turtle.Prelude" provides two `Managed` utilities for creating temporary+-- directories or files:+--+-- @+-- `mktempdir`+-- :: FilePath -- Parent directory+-- -> Text -- Directory name template+-- -> `Managed` FilePath -- Temporary directory+-- @+--+-- @+-- `mktemp`+-- :: FilePath -- Parent directory+-- -> Text -- File name template+-- -> `Managed` (FilePath, Handle) -- Temporary file+-- @+--+-- You can acquire a `Managed` resource within a `Shell` with `using`:+--+-- @+-- `using` :: Managed a -> Shell a+-- @+--+-- ... and here is an example of creating a temporary directory and file within+-- a `Shell`:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > main = sh (do+-- > dir <- using (mktempdir "/tmp" "turtle")+-- > (file, _) <- using (mktemp dir "turtle")+-- > liftIO (print file) )+--+-- When you run the above script it will print out the name of the temporary+-- directory and file:+--+-- > $ ./example.hs+-- > FilePath "/tmp/turtle15976/turtle15976"+--+-- ... and you can verify that they were deleted afterwards:+--+-- > Turtle Prelude> view (find (has "turtle") "/tmp")+-- > Turtle Prelude> -- No results+--+-- As an exercise, try inserting an exception and verifying that the temporary:+-- file and directory are still cleaned up correctly:+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > main = sh (do+-- > dir <- using (mktempdir "/tmp" "turtle")+-- > (file, _) <- using (mktemp dir "turtle")+-- > liftIO (print file)+-- > liftIO (die "Urk!") )+--+-- To learn more about `Managed` resources, read the documentation in+-- "Control.Monad.Managed".++-- $monadio+--+-- If you are sick of having type `liftIO` everywhere, you can omit it. This+-- is because all subroutines in @turtle@ are overloaded using the `MonadIO`+-- type class, like our original `pwd` command where we first encountered the+-- the `MonadIO` type:+--+-- @+-- Prelude Turtle> :type pwd+-- pwd :: `MonadIO` io => io Turtle.`Turtle.FilePath`+-- @+--+-- This means you can this command is overloaded to run in any context that+-- implements the `MonadIO` interface, including:+--+-- * `IO` (obviously)+--+-- * `Shell`+--+-- * `Managed`+--+-- You can tell if a type constructor like `Shell` implements `MonadIO` by+-- clicking the link to the type constructor and looking for the instance list.+-- There you will see a list of instances like:+--+-- > Monad Shell+-- > Functor Shell+-- > MonadPlus Shell+-- > Applicative Shell+-- > Alternative Shell+-- > MonadIO Shell+-- > ...+-- +-- These instances represent the overloaded functions associated with `Shell`+-- and we can see from the list that `Shell` implements `MonadIO` so we can+-- use `pwd` (or any other subroutine in this library) within a `Shell`.+--+-- However, not all subroutines in the Haskell ecosystem are overloaded in this+-- 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 stack+-- > -- stack --install-ghc runghc --package turtle+-- >+-- > -- 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+-- > Test+--+-- 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:+--+--+-- > #!/usr/bin/env stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > import Prelude hiding (FilePath)+-- > +-- > parser :: Parser (FilePath, Maybe FilePath)+-- > parser = (,) <$> argPath "src" "The source file"+-- > <*> optional (argPath "dest" "The destination file")+-- > +-- > main = do+-- > (src, mDest) <- options "A simple `cp` utility" parser+-- > case mDest of+-- > Nothing -> input src & stdout+-- > Just dest -> 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 stack+-- > -- stack --install-ghc runghc --package turtle+-- > +-- > {-# 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+-- you would like to learn more advanced tricks, take the time to read the+-- documentation in these modules:+--+-- * "Turtle.Prelude"+--+-- * "Turtle.Format"+--+-- * "Turtle.Pattern"+--+-- * "Turtle.Shell"+--+-- * "Control.Foldl"+--+-- * "Control.Monad.Managed"+--+-- If you have more questions or need help learning the library, ask a question+-- on Stack Overflow under the @haskell-turtle@ tag. For bugs or feature+-- requests, create an issue on Github at+-- <https://github.com/Gabriel439/Haskell-Turtle-Library/issues>+--+-- 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!
test/Main.hs view
@@ -1,6 +1,6 @@-module Main where - -import Test.DocTest - -main :: IO () -main = doctest ["src/Turtle/Pattern.hs", "src/Turtle/Format.hs"] +module Main where++import Test.DocTest++main :: IO ()+main = doctest ["src/Turtle/Pattern.hs", "src/Turtle/Format.hs"]
turtle.cabal view
@@ -1,100 +1,100 @@-Name: turtle -Version: 1.2.0 -Cabal-Version: >=1.10 -Build-Type: Simple -License: BSD3 -License-File: LICENSE -Copyright: 2015 Gabriel Gonzalez -Author: Gabriel Gonzalez -Maintainer: Gabriel439@gmail.com -Bug-Reports: https://github.com/Gabriel439/Haskell-Turtle-Library/issues -Synopsis: Shell programming, Haskell-style -Description: @turtle@ is a reimplementation of the Unix command line environment - in Haskell so that you can use Haskell as both a shell and a scripting - language. - . - Features include: - . - * Batteries included: Command an extended suite of predefined utilities - . - * Interoperability: You can still run external shell commands - . - * Portability: Works on Windows, OS X, and Linux - . - * Exception safety: Safely acquire and release resources - . - * Streaming: Transform or fold command output in constant space - . - * Patterns: Use typed regular expressions that can parse structured values - . - * Formatting: Type-safe @printf@-style text formatting - . - * Modern: Supports @text@ and @system-filepath@ - . - Read "Turtle.Tutorial" for a detailed tutorial or "Turtle.Prelude" for a - quick-start guide - . - @turtle@ is designed to be beginner-friendly, but as a result lacks certain - features, like tracing commands. If you feel comfortable using @turtle@ - then you should also check out the @Shelly@ library which provides similar - functionality. -Category: System -Source-Repository head - Type: git - Location: https://github.com/Gabriel439/Haskell-Turtle-Library - -Library - HS-Source-Dirs: src - Build-Depends: - base >= 4.5 && < 5 , - async >= 2.0.0.0 && < 2.1, - 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, - system-fileio >= 0.2.1 && < 0.4, - temporary < 1.3, - text < 1.3, - time < 1.6, - 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 - Build-Depends: unix >= 2.5.1.0 && < 2.8 - Exposed-Modules: - Turtle, - Turtle.Format, - Turtle.Pattern, - Turtle.Shell, - Turtle.Options, - Turtle.Prelude, - Turtle.Tutorial - GHC-Options: -O2 -Wall - Default-Language: Haskell2010 - -test-suite tests - Type: exitcode-stdio-1.0 - HS-Source-Dirs: test - Main-Is: Main.hs - GHC-Options: -O2 -Wall - Default-Language: Haskell2010 - Build-Depends: - base >= 4 && < 5 , - doctest >= 0.9.12 && < 0.11 - -benchmark bench - Type: exitcode-stdio-1.0 - HS-Source-Dirs: bench - Main-Is: Main.hs - GHC-Options: -O2 -Wall -threaded - Default-Language: Haskell2010 - Build-Depends: - base >= 4 && < 5 , - criterion >= 1.1.0.0 && < 2 , - text < 1.3, - turtle +Name: turtle+Version: 1.2.1+Cabal-Version: >=1.10+Build-Type: Simple+License: BSD3+License-File: LICENSE+Copyright: 2015 Gabriel Gonzalez+Author: Gabriel Gonzalez+Maintainer: Gabriel439@gmail.com+Bug-Reports: https://github.com/Gabriel439/Haskell-Turtle-Library/issues+Synopsis: Shell programming, Haskell-style+Description: @turtle@ is a reimplementation of the Unix command line environment+ in Haskell so that you can use Haskell as both a shell and a scripting+ language.+ .+ Features include:+ .+ * Batteries included: Command an extended suite of predefined utilities+ .+ * Interoperability: You can still run external shell commands+ .+ * Portability: Works on Windows, OS X, and Linux+ .+ * Exception safety: Safely acquire and release resources + .+ * Streaming: Transform or fold command output in constant space+ .+ * Patterns: Use typed regular expressions that can parse structured values+ .+ * Formatting: Type-safe @printf@-style text formatting+ .+ * Modern: Supports @text@ and @system-filepath@+ .+ Read "Turtle.Tutorial" for a detailed tutorial or "Turtle.Prelude" for a+ quick-start guide+ .+ @turtle@ is designed to be beginner-friendly, but as a result lacks certain+ features, like tracing commands. If you feel comfortable using @turtle@+ then you should also check out the @Shelly@ library which provides similar+ functionality.+Category: System+Source-Repository head+ Type: git+ Location: https://github.com/Gabriel439/Haskell-Turtle-Library++Library+ HS-Source-Dirs: src+ Build-Depends:+ base >= 4.5 && < 5 ,+ async >= 2.0.0.0 && < 2.1,+ 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,+ system-fileio >= 0.2.1 && < 0.4,+ temporary < 1.3,+ text < 1.3,+ time < 1.6,+ 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+ Build-Depends: unix >= 2.5.1.0 && < 2.8+ Exposed-Modules:+ Turtle,+ Turtle.Format,+ Turtle.Pattern,+ Turtle.Shell,+ Turtle.Options,+ Turtle.Prelude,+ Turtle.Tutorial+ GHC-Options: -O2 -Wall+ Default-Language: Haskell2010++test-suite tests+ Type: exitcode-stdio-1.0+ HS-Source-Dirs: test+ Main-Is: Main.hs+ GHC-Options: -O2 -Wall+ Default-Language: Haskell2010+ Build-Depends:+ base >= 4 && < 5 ,+ doctest >= 0.9.12 && < 0.11++benchmark bench+ Type: exitcode-stdio-1.0+ HS-Source-Dirs: bench+ Main-Is: Main.hs+ GHC-Options: -O2 -Wall -threaded+ Default-Language: Haskell2010+ Build-Depends:+ base >= 4 && < 5 ,+ criterion >= 1.1.0.0 && < 2 ,+ text < 1.3,+ turtle