turtle 1.3.6 → 1.6.2
raw patch · 16 files changed
Files
- CHANGELOG.md +248/−0
- LICENSE +2/−2
- bench/Main.hs +1/−1
- src/Turtle.hs +51/−17
- src/Turtle/Bytes.hs +175/−22
- src/Turtle/Format.hs +14/−5
- src/Turtle/Internal.hs +236/−3
- src/Turtle/Line.hs +5/−0
- src/Turtle/Options.hs +45/−7
- src/Turtle/Pattern.hs +13/−3
- src/Turtle/Prelude.hs +665/−203
- src/Turtle/Shell.hs +123/−33
- src/Turtle/Tutorial.hs +103/−39
- test/cptree.hs +2/−2
- test/system-filepath.hs +226/−0
- turtle.cabal +74/−35
CHANGELOG.md view
@@ -1,3 +1,251 @@+1.6.2++* Build against latest `ansi-wl-pprint` and `optparse-applicative` [[#445]](https://github.com/Gabriella439/turtle/pull/445) / [[#446]](https://github.com/Gabriella439/turtle/pull/446) / [[#447]](https://github.com/Gabriella439/turtle/pull/447)++1.6.1++* BUG FIX: Fix `turtle` to build on Windows+* BUG FIX: `stripPrefix` and `commonPrefix` now correctly handle files with+ extensions+ * For example, before this fix `stripPrefix "./" "./foo.bar"` would+ return `Just "foo/.bar"`++1.6.0++* BREAKING CHANGE: Switch to the `FilePath` type from `base` instead of+ `system-filepath`+ * This is a breaking change for a couple of reasons:+ * The `FilePath` type has changed, so the API is not backwards-compatible+ * The thing most likely to break is if you directly imported utilities+ from the `system-filepath` or `system-fileio` packages to operate on+ `turtle`'s `FilePath`s+ * If that happens, you should first check if the `Turtle` module+ exports a utility of the same name. If so, then switch to that+ * If there is no equivalent substitute from the `Turtle` module then+ you will have to change your code to use the closest equivalent+ utility from the `filepath` or `directory` package+ * If you were previously using any of the `system-filepath` or+ `system-fileio` utilities re-exported from the `Turtle` module then+ those utilities will not break as they have been replaced with+ versions compatible with the `FilePath` type from `base`+ * The second thing most likely to break is any code that relies on+ typeclasses since because if you defined any instances for the+ `FilePath` type exported by `turtle` then those instances will now+ overlap with any instances defined for the `String` type+ * The conversion utilities (e.g. `toText`, `encodeString`) will still+ work, so code that used those conversion utilities should be less+ affected by this change+ * The behavior of the `collapse` utility is subtly different+ * `collapse` no longer interprets `..` in paths+ * This new behavior is more correct in the presence of symlinks, so the+ change is (hopefully) an improvement to downstream code+ * The new API strives to match the old behavior as closely as possible+ * … so this should (hopefully) not break too much code in practice+ * With the exception of the `collapse` function the new API should be+ bug-for-bug compatible with the old API+ * Most of the surprising behavior inherited from the old API is around+ how `.` and `..` are handled in paths+ * `parent ".." == "."` is an example of such surprising behavior+ * At some point in the future we may fix bugs in these utilities inherited+ from `system-filepath` / `system-fileio`, but no decision either way has+ been made, yet+ * Some old utilities are marked `DEPRECATED` if their behavior exactly matches+ the behavior of an existing utility from the `filepath` or `directory`+ package+ * These may be eventually removed at some point in the future or they+ remain in a deprecated state indefinitely. No decision either way has+ been made+ * The `Turtle` module also re-exports any utility suggested by a+ `DEPRECATED` pragma as a convenience+ * Other utilities are not deprecated if the old behavior significantly departs+ from any existing utility from the `filepath` or `directory` package+ * For example, the behavior of the `filename` utility differs from the+ behavior of `System.FilePath.takeFileName` for filenames that begin with a+ `.`, so we have to preserve the old behavior to avoid breaking downstream+ code+ * At some point in the future utilities like these may be deprecated in+ favor of their closest analogs in the `filepath` / `directory` packages or+ they may be supported indefinitely. No decision either way has been made+ * If you want to try to author code that is compatible with both the+ pre-1.6 and post-1.6 API:+ * If you add any instances to the `FilePath` type, import it qualified+ directly from the `system-filepath` package and use it only for instances+ * Otherwise, don't import anything else from the `system-filepath` /+ `system-fileio` packages if you can help it. Instead, restrict yourself+ entirely to the utilities and `FilePath` type exported by the `Turtle`+ module+ * Use the conversion utilities (e.g. `encodeStrings`, even if they are not+ necessary post-1.6)+ * If that's still not enough, use `CPP` and good luck!++1.5.25++* Build against latest version of `Win32` package++1.5.24++* Expose `Format` constructor++1.5.23++* Add `fromIO` utility+* Build against GHC 9.0 / 9.2++1.5.22++* Add new `update` utility+* Improve documentation for `limit`++1.5.21++* Build against `optparse-applicative-0.16.0.0`++1.5.20++* Build against `doctest-0.17`+* Only depend on `semigroups` for GHC < 8.0++1.5.19++* Add pattern synonyms for `Size`++1.5.18++* Fix space leak++1.5.17++* Add `optionsExt`: Extended version of `options` with header, footer,+ porgram-description and version information in `--help` flag+* Add `readlink`++1.5.16++* Add `cptreeL`++1.5.15++* Add `toLines`+* Add `Turtle.Bytes.{fromUTF8,toUTF8}`+* Add `Turtle.Bytes.{compress,decompress}`+* Always expose a `MonadFail` instance, relying on the `fail` package+ where needed. Related GHC 8.8 preparedness.++1.5.14++* Fix `cptree` to copy symlinks instead of descending into them+ * See: https://github.com/Gabriel439/Haskell-Turtle-Library/pull/344+* Build against newer versions of `Win32` package++1.5.13++* Fix `chmod` bug+ * See: https://github.com/Gabriel439/Haskell-Turtle-Library/pull/337+* Add `reduce` and re-export `(<&>)`+ * See: https://github.com/Gabriel439/Haskell-Turtle-Library/pull/332++1.5.12++* Increase upper bound on `containers`++1.5.11++* Don't forward broken pipe exceptions when using `inproc`+ * See: https://github.com/Gabriel439/Haskell-Turtle-Library/pull/321+* Increase upper bound on `stm`+ * See: https://github.com/Gabriel439/Haskell-Turtle-Library/pull/321+* Tutorial improvements:+ * See: https://github.com/Gabriel439/Haskell-Turtle-Library/pull/322++1.5.10++* Increase upper bound on `doctest` and `criterion`++1.5.9++* Add `symlink`++1.5.8++* Bug fix: `invert` no longer rejects inputs where a prefix matches the inverted+ pattern+ * See: https://github.com/Gabriel439/Haskell-Turtle-Library/pull/297+* Add lsdepth, findtree, cmin, and cmax+* Increase upper bound on `temporary` and `foldl`++1.5.7++* Increase upper bound on `doctest`++1.5.6++* Increase upper bound on `exceptions`++1.5.5++* Increase upper bound on `criterion`++1.5.4++* Increase upper bound on `exceptions`++1.5.3++* Increase upper bound on `doctest`++1.5.2++* Increase upper bound on `async`++1.5.1++* GHC 8.4 support+* Re-export `encodeString`/`decodeString`+* Update tutorial to use `stack script`+* Increase upper bounds on dependencies++1.5.0++* BREAKING CHANGE: Add `MonadCatch` instance for `Shell`+ * This requires a breaking change to the internal implementation of `Shell`+ * Most breaking changes can be fixed by replacing the `Shell` constructor+ with the newly added `_Shell` utility for ease of migration+ * If you don't use the `Shell` constructor then this change likely does not+ affect you+* Add `eprintf`++1.4.5++* Add `grepText`, `uniq`, `nub`, `sort` to `Turtle.Prelude`+* Increase upper bound on `unix-compat`++1.4.4++* Fix small mistake in tutorial++1.4.3++* Increase upper bound on `doctest`++1.4.2++* Add `sed{Prefix,Suffix,Entire}` and `inplace{Prefix,Suffix,Entire}`++1.4.1++* Increase upper bound on `doctest`++1.4.0++* BREAKING CHANGE: Remove unnecessary `Maybe` from type of `single`+* BREAKING CHANGE: Consolidate `searchable` and `executable`+* `stream{,WithErr}` now throws an `ExitCode` on failure++1.3.6++* Build against `ghc-8.2`+* Relax upper bound on `optparse-applicative` and `foldl`+ 1.3.5 * Increase upper bound on `foldl`
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2017 Gabriel Gonzalez+Copyright (c) 2017 Gabriella Gonzalez All rights reserved. Redistribution and use in source and binary forms, with or without modification,@@ -8,7 +8,7 @@ * 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+ * Neither the name of Gabriella Gonzalez nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission.
bench/Main.hs view
@@ -2,7 +2,7 @@ module Main where import qualified Data.Text as Text-import Criterion.Main+import Test.Tasty.Bench import Turtle boundedNaive :: Int -> Int -> Pattern a -> Pattern [a]
src/Turtle.hs view
@@ -47,8 +47,6 @@ -- -- "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@@ -61,8 +59,6 @@ -- * "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@@ -77,7 +73,8 @@ , module Control.Monad.IO.Class , module Data.Monoid , module Control.Monad.Managed- , module Filesystem.Path.CurrentOS+ , module System.FilePath+ , module Turtle.Internal , Fold(..) , FoldM(..) , Text@@ -87,6 +84,7 @@ , ExitCode(..) , IsString(..) , (&)+ , (<&>) ) where import Turtle.Format@@ -119,9 +117,23 @@ import Control.Monad.IO.Class (MonadIO(..)) import Data.Monoid (Monoid(..), (<>)) import Data.String (IsString(..))-import Filesystem.Path.CurrentOS+import Control.Monad.Managed (Managed, managed, runManaged, with)+import Control.Foldl (Fold(..), FoldM(..))+import Data.Text (Text)+import Data.Time (NominalDiffTime, UTCTime)+import System.FilePath ( FilePath- , root+ , dropExtension+ , hasExtension+ , isAbsolute+ , isRelative+ , (</>)+ , (<.>)+ )+import System.IO (Handle)+import System.Exit (ExitCode(..))+import Turtle.Internal+ ( root , directory , parent , filename@@ -129,25 +141,18 @@ , basename , absolute , relative- , (</>) , commonPrefix , stripPrefix , collapse , splitDirectories , extension- , hasExtension- , (<.>)- , dropExtension , splitExtension+ , splitExtensions , toText , fromText+ , encodeString+ , decodeString )-import Control.Monad.Managed (Managed, managed, runManaged, with)-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 __GLASGOW_HASKELL__ >= 710@@ -160,4 +165,33 @@ -- application operator '$', which allows '&' to be nested in '$'. (&) :: a -> (a -> b) -> b x & f = f x+#endif++#if __GLASGOW_HASKELL__ >= 821+import Data.Functor ((<&>))+#else+-- | Flipped version of '<$>'.+--+-- @+-- ('<&>') = 'flip' 'fmap'+-- @+--+-- @since 4.11.0.0+--+-- ==== __Examples__+-- Apply @(+1)@ to a list, a 'Data.Maybe.Just' and a 'Data.Either.Right':+--+-- >>> Just 2 <&> (+1)+-- Just 3+--+-- >>> [1,2,3] <&> (+1)+-- [2,3,4]+--+-- >>> Right 3 <&> (+1)+-- Right 4+--+(<&>) :: Functor f => f a -> (a -> b) -> f b+as <&> f = f <$> as++infixl 1 <&> #endif
src/Turtle/Bytes.hs view
@@ -16,6 +16,12 @@ , append , stderr , strict+ , compress+ , decompress+ , WindowBits(..)+ , Zlib.defaultWindowBits+ , fromUTF8+ , toUTF8 -- * Subprocess management , proc@@ -40,18 +46,18 @@ import Control.Applicative import Control.Concurrent.Async (Async, Concurrently(..))-import Control.Foldl (FoldM(..)) import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.Managed (MonadManaged(..)) import Data.ByteString (ByteString)+import Data.Monoid+import Data.Streaming.Zlib (Inflate, Popper, PopperRes(..), WindowBits(..)) import Data.Text (Text)-import Filesystem.Path (FilePath)-import Prelude hiding (FilePath)+import Data.Text.Encoding (Decoding(..)) import System.Exit (ExitCode(..)) import System.IO (Handle) import Turtle.Internal (ignoreSIGPIPE) import Turtle.Prelude (ProcFailed(..), ShellFailed(..))-import Turtle.Shell (Shell(..), fold, sh)+import Turtle.Shell (Shell(..), FoldShell(..), fold, sh) import qualified Control.Concurrent.Async as Async import qualified Control.Concurrent.STM as STM@@ -62,7 +68,10 @@ import qualified Control.Monad import qualified Control.Monad.Managed as Managed import qualified Data.ByteString+import qualified Data.Streaming.Zlib as Zlib import qualified Data.Text+import qualified Data.Text.Encoding as Encoding+import qualified Data.Text.Encoding.Error as Encoding.Error import qualified Foreign import qualified System.IO import qualified System.Process as Process@@ -89,8 +98,7 @@ The chunks are not necessarily aligned to line boundaries -} inhandle :: Handle -> Shell ByteString-inhandle handle = Shell (\(FoldM step begin done) -> do- x0 <- begin+inhandle handle = Shell (\(FoldShell step begin done) -> do let loop x = do eof <- System.IO.hIsEOF handle if eof@@ -99,7 +107,7 @@ bytes <- Data.ByteString.hGetSome handle defaultChunkSize x' <- step x bytes loop $! x'- loop $! x0 )+ loop $! begin ) where -- Copied from `Data.ByteString.Lazy.Internal` defaultChunkSize :: Int@@ -485,9 +493,18 @@ -- ^ Chunks of bytes read from process output inshell cmd = stream (Process.shell (Data.Text.unpack cmd)) +waitForProcessThrows :: Process.ProcessHandle -> IO ()+waitForProcessThrows ph = do+ exitCode <- Process.waitForProcess ph+ case exitCode of+ ExitSuccess -> return ()+ ExitFailure _ -> Exception.throwIO exitCode+ {-| `stream` generalizes `inproc` and `inshell` by allowing you to supply your own custom `CreateProcess`. This is for advanced users who feel comfortable using the lower-level @process@ API++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} stream :: Process.CreateProcess@@ -512,26 +529,28 @@ mvar <- liftIO (MVar.newMVar False) let close handle = do MVar.modifyMVar_ mvar (\finalized -> do- Control.Monad.unless finalized (System.IO.hClose handle)+ Control.Monad.unless finalized (ignoreSIGPIPE (System.IO.hClose handle)) return True ) (hIn, hOut, ph) <- using (Managed.managed (Exception.bracket open (\(hIn, _, ph) -> close hIn >> Process.terminateProcess ph))) let feedIn :: (forall a. IO a -> IO a) -> IO () feedIn restore =- restore (sh (do+ restore (ignoreSIGPIPE (sh (do bytes <- s- liftIO (Data.ByteString.hPut hIn bytes) ) )+ liftIO (Data.ByteString.hPut hIn bytes) ) ) ) `Exception.finally` close hIn a <- using (Managed.managed (\k -> Exception.mask (\restore -> Async.withAsync (feedIn restore) k ) ))- inhandle hOut <|> (liftIO (Process.waitForProcess ph *> halt a) *> empty)+ inhandle hOut <|> (liftIO (waitForProcessThrows ph *> halt a) *> empty) {-| `streamWithErr` generalizes `inprocWithErr` and `inshellWithErr` by allowing you to supply your own custom `CreateProcess`. This is for advanced users who feel comfortable using the lower-level @process@ API++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} streamWithErr :: Process.CreateProcess@@ -556,15 +575,15 @@ mvar <- liftIO (MVar.newMVar False) let close handle = do MVar.modifyMVar_ mvar (\finalized -> do- Control.Monad.unless finalized (System.IO.hClose handle)+ Control.Monad.unless finalized (ignoreSIGPIPE (System.IO.hClose handle)) return True ) (hIn, hOut, hErr, ph) <- using (Managed.managed (Exception.bracket open (\(hIn, _, _, ph) -> close hIn >> Process.terminateProcess ph))) let feedIn :: (forall a. IO a -> IO a) -> IO () feedIn restore =- restore (sh (do+ restore (ignoreSIGPIPE (sh (do bytes <- s- liftIO (Data.ByteString.hPut hIn bytes) ) )+ liftIO (Data.ByteString.hPut hIn bytes) ) ) ) `Exception.finally` close hIn queue <- liftIO TQueue.newTQueueIO@@ -580,8 +599,7 @@ bytes <- inhandle hErr liftIO (STM.atomically (TQueue.writeTQueue queue (Just (Left bytes)))) )) `Exception.finally` STM.atomically (TQueue.writeTQueue queue Nothing)- let drain = Shell (\(FoldM step begin done) -> do- x0 <- begin+ let drain = Shell (\(FoldShell step begin done) -> do let loop x numNothing | numNothing < 2 = do m <- STM.atomically (TQueue.readTQueue queue)@@ -591,7 +609,7 @@ x' <- step x e loop x' numNothing | otherwise = return x- x1 <- loop x0 (0 :: Int)+ x1 <- loop begin (0 :: Int) done x1 ) a <- using@@ -611,12 +629,13 @@ _ <- r return () let waitAll = STM.atomically (Async.waitSTM a `also` (Async.waitSTM b `also` Async.waitSTM c))- drain <|> (liftIO (Process.waitForProcess ph *> waitAll) *> empty)+ drain <|> (liftIO (waitForProcessThrows ph *> waitAll) *> empty) {-| Run a command using the shell, streaming @stdout@ and @stderr@ as chunks of `ByteString`. Chunks from @stdout@ are wrapped in `Right` and chunks from- @stderr@ are wrapped in `Left`. This does /not/ throw an exception if the- command returns a non-zero exit code+ @stderr@ are wrapped in `Left`.++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} inprocWithErr :: Text@@ -633,11 +652,12 @@ {-| Run a command line using the shell, streaming @stdout@ and @stderr@ as chunks of `ByteString`. Chunks from @stdout@ are wrapped in `Right` and- chunks from @stderr@ are wrapped in `Left`. This does /not/ throw an- exception if the command returns a non-zero exit code+ chunks from @stderr@ are wrapped in `Left`. This command is more powerful than `inprocWithErr`, but highly vulnerable to code injection if you template the command line with untrusted input++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} inshellWithErr :: Text@@ -647,3 +667,136 @@ -> Shell (Either ByteString ByteString) -- ^ Chunks of either output (`Right`) or error (`Left`) inshellWithErr cmd = streamWithErr (Process.shell (Data.Text.unpack cmd))++-- | Internal utility used by both `compress` and `decompress`+fromPopper :: Popper -> Shell ByteString+fromPopper popper = loop+ where+ loop = do+ result <- liftIO popper++ case result of+ PRDone ->+ empty+ PRNext compressedByteString ->+ return compressedByteString <|> loop+ PRError exception ->+ liftIO (Exception.throwIO exception)++{-| Compress a stream using @zlib@++ Note that this can decompress streams that are the concatenation of+ multiple compressed streams (just like @gzip@)++>>> let compressed = select [ "ABC", "DEF" ] & compress 0 defaultWindowBits+>>> compressed & decompress defaultWindowBits & view+"ABCDEF"+>>> (compressed <|> compressed) & decompress defaultWindowBits & view+"ABCDEF"+"ABCDEF"+-}+compress+ :: Int+ -- ^ Compression level+ -> WindowBits+ -- ^+ -> Shell ByteString+ -- ^+ -> Shell ByteString+compress compressionLevel windowBits bytestrings = do+ deflate <- liftIO (Zlib.initDeflate compressionLevel windowBits)++ let loop = do+ bytestring <- bytestrings++ popper <- liftIO (Zlib.feedDeflate deflate bytestring)++ fromPopper popper++ let wrapUp = do+ let popper = liftIO (Zlib.finishDeflate deflate)++ fromPopper popper++ loop <|> wrapUp++data DecompressionState = Uninitialized | Decompressing Inflate++-- | Decompress a stream using @zlib@ (just like the @gzip@ command)+decompress :: WindowBits -> Shell ByteString -> Shell ByteString+decompress windowBits (Shell k) = Shell k'+ where+ k' (FoldShell step begin done) = k (FoldShell step' begin' done')+ where+ begin' = (begin, Uninitialized)++ step' (x0, Uninitialized) compressedByteString = do+ inflate <- Zlib.initInflate windowBits++ step' (x0, Decompressing inflate) compressedByteString+ step' (x0, Decompressing inflate) compressedByteString = do+ popper <- Zlib.feedInflate inflate compressedByteString++ let loop x = do+ result <- popper++ case result of+ PRDone -> do+ compressedByteString' <- Zlib.getUnusedInflate inflate++ if Data.ByteString.null compressedByteString'+ then return (x, Decompressing inflate)+ else do+ decompressedByteString <- Zlib.finishInflate inflate++ x' <- step x decompressedByteString++ step' (x', Uninitialized) compressedByteString'+ PRNext decompressedByteString -> do+ x' <- step x decompressedByteString++ loop x'+ PRError exception -> do+ Exception.throwIO exception++ loop x0++ done' (x0, Uninitialized) = do+ done x0+ done' (x0, Decompressing inflate) = do+ decompressedByteString <- Zlib.finishInflate inflate++ x0' <- step x0 decompressedByteString++ done' (x0', Uninitialized)++{-| Decode a stream of bytes as UTF8 `Text`++ NOTE: This function will throw a pure exception (i.e. an `error`) if UTF8+ decoding fails (mainly due to limitations in the @text@ package's stream+ decoding API)+-}+toUTF8 :: Shell ByteString -> Shell Text+toUTF8 (Shell k) = Shell k'+ where+ k' (FoldShell step begin done) =+ k (FoldShell step' begin' done')+ where+ begin' =+ (mempty, Encoding.streamDecodeUtf8With Encoding.Error.strictDecode, begin)++ step' (prefix, decoder, x) suffix = do+ let bytes = prefix <> suffix++ let Some text prefix' decoder' = decoder bytes ++ x' <- step x text++ return (prefix', decoder', x')++ done' (_, _, x) = do+ done x++-- | Encode a stream of bytes as UTF8 `Text`+fromUTF8 :: Shell Text -> Shell ByteString+fromUTF8 = fmap Encoding.encodeUtf8
src/Turtle/Format.hs view
@@ -42,10 +42,11 @@ module Turtle.Format ( -- * Format- Format+ Format (..) , (%) , format , printf+ , eprintf , makeFormat -- * Parameters@@ -73,9 +74,9 @@ import Data.Text (Text, pack) import Data.Time (UTCTime) import Data.Word-import Filesystem.Path.CurrentOS (FilePath, toText) import Numeric (showEFloat, showFFloat, showGFloat, showHex, showOct)-import Prelude hiding ((.), id, FilePath)+import Prelude hiding ((.), id)+import qualified System.IO as IO import Turtle.Line (Line) import qualified Data.Text.IO as Text@@ -113,6 +114,14 @@ printf :: MonadIO io => Format (io ()) r -> r printf fmt = fmt >>- (liftIO . Text.putStr) +{-| Print a `Format` string to standard err (without a trailing newline)++>>> eprintf ("Hello, "%s%"!\n") "world"+Hello, world!+-}+eprintf :: MonadIO io => Format (io ()) r -> r+eprintf fmt = fmt >>- (liftIO . Text.hPutStr IO.stderr)+ -- | Create your own format specifier makeFormat :: (a -> Text) -> Format r (a -> r) makeFormat k = Format (\return_ -> \a -> return_ (k a))@@ -205,9 +214,9 @@ l :: Format r (Line -> r) l = makeFormat Turtle.Line.lineToText --- | `Format` a `Filesystem.Path.CurrentOS.FilePath` into `Text`+-- | `Format` a `FilePath` into `Text` fp :: Format r (FilePath -> r)-fp = makeFormat (\fpath -> either id id (toText fpath))+fp = makeFormat pack -- | `Format` a `UTCTime` into `Text` utc :: Format r (UTCTime -> r)
src/Turtle/Internal.hs view
@@ -1,11 +1,17 @@-module Turtle.Internal- ( ignoreSIGPIPE- ) where+module Turtle.Internal where +import Control.Applicative ((<|>)) import Control.Exception (handle, throwIO)+import Data.Text (Text) import Foreign.C.Error (Errno(..), ePIPE) import GHC.IO.Exception (IOErrorType(..), IOException(..))+import System.FilePath ((</>)) +import qualified Data.List as List+import qualified Data.Text as Text+import qualified Data.Text.IO as Text.IO+import qualified System.FilePath as FilePath+ ignoreSIGPIPE :: IO () -> IO () ignoreSIGPIPE = handle (\e -> case e of IOError@@ -14,3 +20,230 @@ | Errno ioe == ePIPE -> return () _ -> throwIO e )++{-| Convert a `FilePath` to human-readable `Text`++ Note that even though the type says `Either` this utility actually always+ succeeds and returns a `Right` value. The only reason for the `Either` is+ compatibility with the old type from the @system-filepath@ package.+-}+toText :: FilePath -> Either Text Text+toText = Right . Text.pack+{-# DEPRECATED toText "Use Data.Text.pack instead" #-}++-- | Convert `Text` to a `FilePath`+fromText :: Text -> FilePath+fromText = Text.unpack+{-# DEPRECATED fromText "Use Data.Text.unpack instead" #-}++-- | Convert a `String` to a `FilePath`+decodeString :: String -> FilePath+decodeString = id+{-# DEPRECATED decodeString "Use id instead" #-}++-- | Convert a `FilePath` to a `String`+encodeString :: FilePath -> String+encodeString = id+{-# DEPRECATED encodeString "Use id instead" #-}++-- | Find the greatest common prefix between a list of `FilePath`s+commonPrefix :: [FilePath] -> FilePath+commonPrefix [ ] = mempty+commonPrefix (path : paths) = foldr longestPathPrefix path paths+ where+ longestPathPrefix left right+ | leftComponents == rightComponents =+ FilePath.joinPath leftComponents+ ++ mconcat (longestPrefix leftExtensions rightExtensions)+ | otherwise =+ FilePath.joinPath (longestPrefix leftComponents rightComponents)+ where+ (leftComponents, leftExtensions) = splitExt (splitDirectories left)++ (rightComponents, rightExtensions) = splitExt (splitDirectories right)++longestPrefix :: Eq a => [a] -> [a] -> [a]+longestPrefix (l : ls) (r : rs)+ | l == r = l : longestPrefix ls rs+longestPrefix _ _ = [ ]++-- | Remove a prefix from a path+stripPrefix :: FilePath -> FilePath -> Maybe FilePath+stripPrefix prefix path = do+ componentSuffix <- List.stripPrefix prefixComponents pathComponents++ if null componentSuffix+ then do+ prefixSuffix <- List.stripPrefix prefixExtensions pathExtensions++ return (mconcat prefixSuffix)+ else do+ return (FilePath.joinPath componentSuffix ++ mconcat pathExtensions)+ where+ (prefixComponents, prefixExtensions) = splitExt (splitDirectories prefix)++ (pathComponents, pathExtensions) = splitExt (splitDirectories path)++-- Internal helper function for `stripPrefix` and `commonPrefix`+splitExt :: [FilePath] -> ([FilePath], [String])+splitExt [ component ] = ([ base ], map ("." ++) exts)+ where+ (base, exts) = splitExtensions component+splitExt [ ] =+ ([ ], [ ])+splitExt (component : components) = (component : base, exts)+ where+ (base, exts) = splitExt components++-- | Normalise a path+collapse :: FilePath -> FilePath+collapse = FilePath.normalise+{-# DEPRECATED collapse "Use System.FilePath.normalise instead" #-}++-- | Read in a file as `Text`+readTextFile :: FilePath -> IO Text+readTextFile = Text.IO.readFile+{-# DEPRECATED readTextFile "Use Data.Text.IO.readFile instead" #-}++-- | Write out a file as `Text`+writeTextFile :: FilePath -> Text -> IO ()+writeTextFile = Text.IO.writeFile+{-# DEPRECATED writeTextFile "Use Data.Text.IO.writeFile instead" #-}++-- | Retrieves the `FilePath`'s root+root :: FilePath -> FilePath+root = fst . FilePath.splitDrive++-- | Retrieves the `FilePath`'s parent directory+parent :: FilePath -> FilePath+parent path = prefix </> suffix+ where+ (drive, rest) = FilePath.splitDrive path++ components = loop (splitDirectories rest)++ prefix =+ case components of+ "./" : _ -> drive+ "../" : _ -> drive+ _ | null drive -> "./"+ | otherwise -> drive++ suffix = FilePath.joinPath components++ loop [ _ ] = [ ]+ loop [ ] = [ ]+ loop (c : cs) = c : loop cs++-- | Retrieves the `FilePath`'s directory+directory :: FilePath -> FilePath+directory path+ | prefix == "" && suffix == ".." =+ "../"+ | otherwise =+ trailingSlash (FilePath.takeDirectory prefix) ++ suffix+ where+ (prefix, suffix) = trailingParent path+ where+ trailingParent ".." = ("" , "..")+ trailingParent [ a, b ] = ([ a, b ], "" )+ trailingParent [ a ] = ([ a ] , "" )+ trailingParent [ ] = ([ ] , "" )+ trailingParent (c : cs) = (c : p, s)+ where+ ~(p, s) = trailingParent cs++ trailingSlash "" = "/"+ trailingSlash "/" = "/"+ trailingSlash (c : cs) = c : trailingSlash cs++-- | Retrieves the `FilePath`'s filename component+filename :: FilePath -> FilePath+filename path+ | result == "." || result == ".." = ""+ | otherwise = result+ where+ result = FilePath.takeFileName path++-- | Retrieve a `FilePath`'s directory name+dirname :: FilePath -> FilePath+dirname path = loop (splitDirectories path)+ where+ loop [ x, y ] =+ case deslash y <|> deslash x of+ Just name -> name+ Nothing -> ""+ loop [ x ] =+ case deslash x of+ Just name -> name+ Nothing -> ""+ loop [ ] =+ ""+ loop (_ : xs) =+ loop xs++ deslash "" = Nothing+ deslash "/" = Just ""+ deslash (c : cs) = fmap (c :) (deslash cs)++-- | Retrieve a `FilePath`'s basename component+basename :: FilePath -> String+basename path =+ case name of+ '.' : _ -> name+ _ ->+ case splitExtensions name of+ (base, _) -> base+ where+ name = filename path++-- | Test whether a path is absolute+absolute :: FilePath -> Bool+absolute = FilePath.isAbsolute+{-# DEPRECATED absolute "Use System.FilePath.isAbsolute instead" #-}++-- | Test whether a path is relative+relative :: FilePath -> Bool+relative = FilePath.isRelative+{-# DEPRECATED relative "Use System.FilePath.isRelative instead" #-}++-- | Split a `FilePath` into its components+splitDirectories :: FilePath -> [FilePath]+splitDirectories path = loop (FilePath.splitPath path)+ where+ loop [ ] = [ ]+ loop [ ".." ] = [ "../" ]+ loop [ "." ] = [ "./" ]+ loop (c : cs) = c : loop cs++-- | Get a `FilePath`'s last extension, or `Nothing` if it has no extension+extension :: FilePath -> Maybe String+extension path =+ case suffix of+ '.' : ext -> Just ext+ _ -> Nothing+ where+ suffix = FilePath.takeExtension path++-- | Split a `FilePath` on its extension+splitExtension :: FilePath -> (String, Maybe String)+splitExtension path =+ case suffix of+ '.' : ext -> (prefix, Just ext)+ _ -> (prefix, Nothing)+ where+ (prefix, suffix) = FilePath.splitExtension path++-- | Split a `FilePath` on its extensions+splitExtensions :: FilePath -> (String, [String])+splitExtensions path0 = (prefix0, reverse exts0)+ where+ (prefix0, exts0) = loop path0++ loop path = case splitExtension path of+ (prefix, Just ext) ->+ (base, ext : exts)+ where+ (base, exts) = loop prefix+ (base, Nothing) ->+ (base, [])
src/Turtle/Line.hs view
@@ -62,6 +62,11 @@ newtype Line = Line Text deriving (Eq, Ord, Show, Monoid) +#if __GLASGOW_HASKELL__ >= 804+instance Semigroup Line where+ (<>) = mappend+#endif+ instance IsString Line where fromString = fromMaybe (throw NewlineForbidden) . textToLine . fromString
src/Turtle/Options.hs view
@@ -17,8 +17,8 @@ -- > -- > main = do -- > (name, age) <- options "Greeting script" parser--- > echo (format ("Hello there, "%s) name)--- > echo (format ("You are "%d%" years old") age)+-- > echo (repr (format ("Hello there, "%s) name))+-- > echo (repr (format ("You are "%d%" years old") age)) -- -- > $ ./options --name John --age 42 -- > Hello there, John@@ -71,6 +71,7 @@ , subcommand , subcommandGroup , options+ , optionsExt ) where @@ -82,9 +83,7 @@ import Data.Optional import Control.Applicative import Control.Monad.IO.Class-import Filesystem.Path.CurrentOS (FilePath, fromText) import Options.Applicative (Parser)-import Prelude hiding (FilePath) import Text.PrettyPrint.ANSI.Leijen (Doc, displayS, renderCompact) import Turtle.Line (Line) @@ -107,6 +106,30 @@ prefs = Opts.showHelpOnError #endif +{-| Parse the given options from the command line and add additional information++ Extended version of @options@ with program version header and footer information+-}+optionsExt :: MonadIO io => Header -> Footer -> Description -> Version -> Parser a -> io a+optionsExt header footer desc version parser = liftIO+ $ Opts.customExecParser (Opts.prefs prefs)+ $ Opts.info (Opts.helper <*> versionOption <*> parser)+ (Opts.headerDoc (Just (getHeader header)) <>+ Opts.footerDoc (Just (getFooter footer)) <>+ Opts.progDescDoc (Just (getDescription desc)))+ where+ versionOption =+ Opts.infoOption+ (Text.unpack version)+ (Opts.long "version" <> Opts.help "Show version")+ prefs :: Opts.PrefsMod+#if MIN_VERSION_optparse_applicative(0,13,0)+ prefs = Opts.showHelpOnError <> Opts.showHelpOnEmpty+#else+ prefs = Opts.showHelpOnError+#endif++ {-| The name of a command-line argument This is used to infer the long name and metavariable for the command line@@ -135,6 +158,21 @@ newtype Description = Description { getDescription :: Doc } deriving (IsString) +{-| Header of the program++ This description will appear in the header of the @--help@ output+-}+newtype Header = Header { getHeader :: Doc }+ deriving (IsString)+{-| Footer of the program++ This description will appear in the footer of the @--help@ output+-}+newtype Footer = Fotter { getFooter :: Doc }+ deriving (IsString)++-- | Program Version+type Version = Text {-| A helpful message explaining what a flag does This will appear in the @--help@ output@@ -197,7 +235,7 @@ -- | 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)+optPath argName short msg = fmap Text.unpack (optText argName short msg) {- | Build a positional argument parser for any type by providing a `Text`-parsing function@@ -237,14 +275,14 @@ -- | Parse a `FilePath` as a positional argument argPath :: ArgName -> Optional HelpMessage -> Parser FilePath-argPath argName msg = fmap fromText (argText argName msg)+argPath argName msg = fmap Text.unpack (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+ Nothing -> Opts.readerAbort (Opts.ShowHelpText Nothing) {-| Create a sub-command that parses `CommandName` and then parses the rest of the command-line arguments
src/Turtle/Pattern.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeFamilies #-}@@ -123,6 +124,11 @@ newtype Pattern a = Pattern { runPattern :: StateT Text [] a } deriving (Functor, Applicative, Monad, Alternative, MonadPlus) +#if __GLASGOW_HASKELL__ >= 804+instance Monoid a => Semigroup (Pattern a) where+ (<>) = mappend+#endif+ instance Monoid a => Monoid (Pattern a) where mempty = pure mempty mappend = liftA2 mappend@@ -438,11 +444,15 @@ [] >>> match (invert "A") "B" [()]+>>> match (invert "A") "AA"+[()] -} invert :: Pattern a -> Pattern ()-invert p = Pattern (StateT (\str -> case runStateT (runPattern p) str of- [] -> [((), "")]- _ -> [] ))+invert p = Pattern (StateT f)+ where+ f str = case match p str of+ [] -> [((), "")]+ _ -> [] {-| Match a `Char`, but return `Text`
src/Turtle/Prelude.hs view
@@ -3,6 +3,9 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ViewPatterns #-} -- | This module provides a large suite of utilities that resemble Unix -- utilities.@@ -108,8 +111,8 @@ echo , err , readline- , Filesystem.readTextFile- , Filesystem.writeTextFile+ , Internal.readTextFile+ , Internal.writeTextFile , arguments #if __GLASGOW_HASKELL__ >= 710 , export@@ -120,12 +123,18 @@ , cd , pwd , home+ , readlink , realpath , mv , mkdir , mktree , cp , cptree+ , cptreeL+#if !defined(mingw32_HOST_OS)+ , symlink+#endif+ , isNotSymbolicLink , rm , rmdir , rmtree@@ -169,12 +178,22 @@ , ls , lsif , lstree+ , lsdepth , cat , grep+ , grepText , sed+ , sedPrefix+ , sedSuffix+ , sedEntire , onFiles , inplace+ , inplacePrefix+ , inplaceSuffix+ , inplaceEntire+ , update , find+ , findtree , yes , nl , paste@@ -184,6 +203,15 @@ , cache , parallel , single+ , uniq+ , uniqOn+ , uniqBy+ , nub+ , nubOn+ , sort+ , sortOn+ , sortBy+ , toLines -- * Folds , countChars@@ -214,7 +242,7 @@ , systemStrictWithErr -- * Permissions- , Permissions+ , Permissions(..) , chmod , getmod , setmod@@ -222,12 +250,11 @@ , readable, nonreadable , writable, nonwritable , executable, nonexecutable- , searchable, nonsearchable- , ooo,roo,owo,oox,oos,rwo,rox,ros,owx,rwx,rws+ , ooo,roo,owo,oox,rwo,rox,owx,rwx -- * File size , du- , Size+ , Size(B, KB, MB, GB, TB, KiB, MiB, GiB, TiB) , sz , bytes , kilobytes@@ -254,6 +281,8 @@ , PosixCompat.isDirectory , PosixCompat.isSymbolicLink , PosixCompat.isSocket+ , cmin+ , cmax -- * Headers , WithHeader(..)@@ -274,26 +303,29 @@ import qualified Control.Concurrent.STM as STM import qualified Control.Concurrent.STM.TQueue as TQueue import Control.Exception (Exception, bracket, bracket_, finally, mask, throwIO)-import Control.Foldl (Fold, FoldM(..), genericLength, handles, list, premap)+import Control.Foldl (Fold(..), genericLength, handles, list, premap) import qualified Control.Foldl import qualified Control.Foldl.Text-import Control.Monad (guard, liftM, msum, when, unless, (>=>))+import Control.Monad (foldM, guard, liftM, msum, when, unless, (>=>), mfilter) import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.Managed (MonadManaged(..), managed, managed_, runManaged) #ifdef mingw32_HOST_OS import Data.Bits ((.&.)) #endif import Data.IORef (newIORef, readIORef, writeIORef)+import qualified Data.List as List+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NonEmpty+import Data.Monoid ((<>))+import Data.Ord (comparing)+import qualified Data.Set as Set import Data.Text (Text, pack, unpack) import Data.Time (NominalDiffTime, UTCTime, getCurrentTime)-import Data.Time.Clock.POSIX (POSIXTime)+import Data.Time.Clock.POSIX (POSIXTime, posixSecondsToUTCTime) import Data.Traversable import qualified Data.Text as Text import qualified Data.Text.IO as Text import Data.Typeable (Typeable)-import qualified Filesystem-import Filesystem.Path.CurrentOS (FilePath, (</>))-import qualified Filesystem.Path.CurrentOS as Filesystem import GHC.IO.Exception (IOErrorType(UnsupportedOperation)) import Network.HostName (getHostName) import System.Clock (Clock(..), TimeSpec(..), getTime)@@ -307,8 +339,9 @@ lookupEnv, #endif getEnvironment )-import System.Directory (Permissions) import qualified System.Directory as Directory+import System.FilePath ((</>))+import qualified System.FilePath as FilePath import System.Exit (ExitCode(..), exitWith) import System.IO (Handle, hClose) import qualified System.IO as IO@@ -325,13 +358,14 @@ readDirStream, closeDirStream, touchFile )+import System.Posix.Files (createSymbolicLink) #endif-import Prelude hiding (FilePath)+import Prelude hiding (lines) import Turtle.Pattern (Pattern, anyChar, chars, match, selfless, sepBy) import Turtle.Shell-import Turtle.Format (Format, format, makeFormat, d, w, (%))-import Turtle.Internal (ignoreSIGPIPE)+import Turtle.Format (Format, format, makeFormat, d, w, (%), fp)+import qualified Turtle.Internal as Internal import Turtle.Line {-| Run a command using @execvp@, retrieving the exit code@@ -531,7 +565,7 @@ mvar <- newMVar False let close handle = do modifyMVar_ mvar (\finalized -> do- unless finalized (ignoreSIGPIPE (hClose handle))+ unless finalized (Internal.ignoreSIGPIPE (hClose handle)) return True ) let close' (Just hIn, ph) = do close hIn@@ -542,7 +576,7 @@ let handle (Just hIn, ph) = do let feedIn :: (forall a. IO a -> IO a) -> IO () feedIn restore =- restore (ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn+ restore (Internal.ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn mask (\restore -> withAsync (feedIn restore) (\a -> restore (Process.waitForProcess ph) `finally` halt a) )@@ -580,13 +614,13 @@ mvar <- newMVar False let close handle = do modifyMVar_ mvar (\finalized -> do- unless finalized (ignoreSIGPIPE (hClose handle))+ unless finalized (Internal.ignoreSIGPIPE (hClose handle)) return True ) bracket open (\(hIn, _, ph) -> close hIn >> Process.terminateProcess ph) (\(hIn, hOut, ph) -> do let feedIn :: (forall a. IO a -> IO a) -> IO () feedIn restore =- restore (ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn+ restore (Internal.ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn concurrently (mask (\restore ->@@ -623,13 +657,13 @@ mvar <- newMVar False let close handle = do modifyMVar_ mvar (\finalized -> do- unless finalized (ignoreSIGPIPE (hClose handle))+ unless finalized (Internal.ignoreSIGPIPE (hClose handle)) return True ) bracket open (\(hIn, _, _, ph) -> close hIn >> Process.terminateProcess ph) (\(hIn, hOut, hErr, ph) -> do let feedIn :: (forall a. IO a -> IO a) -> IO () feedIn restore =- restore (ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn+ restore (Internal.ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn runConcurrently $ (,,) <$> Concurrently (mask (\restore ->@@ -659,6 +693,8 @@ injection if you template the command line with untrusted input The command inherits @stderr@ for the current process++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} inshell :: Text@@ -669,9 +705,18 @@ -- ^ Lines of standard output inshell cmd = stream (Process.shell (unpack cmd)) +waitForProcessThrows :: Process.ProcessHandle -> IO ()+waitForProcessThrows ph = do+ exitCode <- Process.waitForProcess ph+ case exitCode of+ ExitSuccess -> return ()+ ExitFailure _ -> Control.Exception.throwIO exitCode+ {-| `stream` generalizes `inproc` and `inshell` by allowing you to supply your own custom `CreateProcess`. This is for advanced users who feel comfortable using the lower-level @process@ API++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} stream :: Process.CreateProcess@@ -696,21 +741,23 @@ mvar <- liftIO (newMVar False) let close handle = do modifyMVar_ mvar (\finalized -> do- unless finalized (hClose handle)+ unless finalized (Internal.ignoreSIGPIPE (hClose handle)) return True ) (hIn, hOut, ph) <- using (managed (bracket open (\(hIn, _, ph) -> close hIn >> Process.terminateProcess ph))) let feedIn :: (forall a. IO a -> IO a) -> IO ()- feedIn restore = restore (outhandle hIn s) `finally` close hIn+ feedIn restore = restore (Internal.ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn a <- using (managed (\k -> mask (\restore -> withAsync (feedIn restore) (restore . k))))- inhandle hOut <|> (liftIO (Process.waitForProcess ph *> halt a) *> empty)+ inhandle hOut <|> (liftIO (waitForProcessThrows ph *> halt a) *> empty) {-| `streamWithErr` generalizes `inprocWithErr` and `inshellWithErr` by allowing you to supply your own custom `CreateProcess`. This is for advanced users who feel comfortable using the lower-level @process@ API++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} streamWithErr :: Process.CreateProcess@@ -735,12 +782,12 @@ mvar <- liftIO (newMVar False) let close handle = do modifyMVar_ mvar (\finalized -> do- unless finalized (hClose handle)+ unless finalized (Internal.ignoreSIGPIPE (hClose handle)) return True ) (hIn, hOut, hErr, ph) <- using (managed (bracket open (\(hIn, _, _, ph) -> close hIn >> Process.terminateProcess ph))) let feedIn :: (forall a. IO a -> IO a) -> IO ()- feedIn restore = restore (outhandle hIn s) `finally` close hIn+ feedIn restore = restore (Internal.ignoreSIGPIPE (outhandle hIn s)) `finally` close hIn queue <- liftIO TQueue.newTQueueIO let forwardOut :: (forall a. IO a -> IO a) -> IO ()@@ -755,8 +802,7 @@ line <- inhandle hErr liftIO (STM.atomically (TQueue.writeTQueue queue (Just (Left line)))) )) `finally` STM.atomically (TQueue.writeTQueue queue Nothing)- let drain = Shell (\(FoldM step begin done) -> do- x0 <- begin+ let drain = Shell (\(FoldShell step begin done) -> do let loop x numNothing | numNothing < 2 = do m <- STM.atomically (TQueue.readTQueue queue)@@ -766,7 +812,7 @@ x' <- step x e loop x' numNothing | otherwise = return x- x1 <- loop x0 (0 :: Int)+ x1 <- loop begin (0 :: Int) done x1 ) a <- using@@ -783,12 +829,13 @@ _ <- r return () let waitAll = STM.atomically (waitSTM a `also` (waitSTM b `also` waitSTM c))- drain <|> (liftIO (Process.waitForProcess ph *> waitAll) *> empty)+ drain <|> (liftIO (waitForProcessThrows ph *> waitAll) *> empty) {-| Run a command using the shell, streaming @stdout@ and @stderr@ as lines of `Text`. Lines from @stdout@ are wrapped in `Right` and lines from @stderr@- are wrapped in `Left`. This does /not/ throw an exception if the command- returns a non-zero exit code+ are wrapped in `Left`.++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} inprocWithErr :: Text@@ -802,14 +849,14 @@ inprocWithErr cmd args = streamWithErr (Process.proc (unpack cmd) (map unpack args)) - {-| Run a command line using the shell, streaming @stdout@ and @stderr@ as lines of `Text`. Lines from @stdout@ are wrapped in `Right` and lines from- @stderr@ are wrapped in `Left`. This does /not/ throw an exception if the- command returns a non-zero exit code+ @stderr@ are wrapped in `Left`. This command is more powerful than `inprocWithErr`, but highly vulnerable to code injection if you template the command line with untrusted input++ Throws an `ExitCode` exception if the command returns a non-zero exit code -} inshellWithErr :: Text@@ -848,7 +895,11 @@ arguments = liftIO (fmap (map pack) getArgs) #if __GLASGOW_HASKELL__ >= 710--- | Set or modify an environment variable+{-| Set or modify an environment variable++ Note: This will change the current environment for all of your program's+ threads since this modifies the global state of the process+-} export :: MonadIO io => Text -> Text -> io () export key val = liftIO (setEnv (unpack key) (unpack val)) @@ -871,9 +922,13 @@ where toTexts (key, val) = (pack key, pack val) --- | Change the current directory+{-| Change the current directory++ Note: This will change the current directory for all of your program's+ threads since this modifies the global state of the process+-} cd :: MonadIO io => FilePath -> io ()-cd path = liftIO (Filesystem.setWorkingDirectory path)+cd path = liftIO (Directory.setCurrentDirectory path) {-| Change the current directory. Once the current 'Shell' is done, it returns back to the original directory.@@ -892,15 +947,19 @@ -- | Get the current directory pwd :: MonadIO io => io FilePath-pwd = liftIO Filesystem.getWorkingDirectory+pwd = liftIO Directory.getCurrentDirectory -- | Get the home directory home :: MonadIO io => io FilePath-home = liftIO Filesystem.getHomeDirectory+home = liftIO Directory.getHomeDirectory +-- | Get the path pointed to by a symlink+readlink :: MonadIO io => FilePath -> io FilePath+readlink path = liftIO (Directory.getSymbolicLinkTarget path)+ -- | Canonicalize a path realpath :: MonadIO io => FilePath -> io FilePath-realpath path = liftIO (Filesystem.canonicalizePath path)+realpath path = liftIO (Directory.canonicalizePath path) #ifdef mingw32_HOST_OS fILE_ATTRIBUTE_REPARSE_POINT :: Win32.FileAttributeOrFlag@@ -914,9 +973,8 @@ @\"..\"@ -} ls :: FilePath -> Shell FilePath-ls path = Shell (\(FoldM step begin done) -> do- x0 <- begin- let path' = Filesystem.encodeString path+ls path = Shell (\(FoldShell step begin done) -> do+ let path' = path canRead <- fmap Directory.readable (Directory.getPermissions (deslash path'))@@ -924,34 +982,32 @@ reparse <- fmap reparsePoint (Win32.getFileAttributes path') if (canRead && not reparse) then bracket- (Win32.findFirstFile (Filesystem.encodeString (path </> "*")))+ (Win32.findFirstFile (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' /= "..")+ file <- Win32.getFindDataFileName fdat+ 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 )+ loop $! begin )+ else done begin ) #else if canRead then bracket (openDirStream path') closeDirStream (\dirp -> do let loop x = do- file' <- readDirStream dirp- case file' of+ file <- readDirStream dirp+ case file of "" -> done x _ -> do- let file = Filesystem.decodeString file'- x' <- if (file' /= "." && file' /= "..")+ x' <- if (file /= "." && file /= "..") then step x (path </> file) else return x loop $! x'- loop $! x0 )- else done x0 )+ loop $! begin )+ else done begin ) #endif {-| This is used to remove the trailing slash from a path, because@@ -974,6 +1030,29 @@ then return child <|> lstree child else return child ++{- | Stream the recursive descendents of a given directory+ between a given minimum and maximum depth+-}+lsdepth :: Int -> Int -> FilePath -> Shell FilePath+lsdepth mn mx path =+ lsdepthHelper 1 mn mx path+ where+ lsdepthHelper :: Int -> Int -> Int -> FilePath -> Shell FilePath+ lsdepthHelper depth l u p = + if depth > u+ then empty+ else do+ child <- ls p+ isDir <- testdir child+ if isDir+ then if depth >= l+ then return child <|> lsdepthHelper (depth + 1) l u child+ else lsdepthHelper (depth + 1) l u child+ else if depth >= l+ then return child+ else empty+ {-| Stream all recursive descendents of the given directory This skips any directories that fail the supplied predicate@@ -999,11 +1078,11 @@ but the operation will not be atomic -} mv :: MonadIO io => FilePath -> FilePath -> io ()-mv oldPath newPath = liftIO $ catchIOError (Filesystem.rename oldPath newPath)+mv oldPath newPath = liftIO $ catchIOError (Directory.renameFile oldPath newPath) (\ioe -> if ioeGetErrorType ioe == UnsupportedOperation -- certainly EXDEV then do- Filesystem.copyFile oldPath newPath- Filesystem.removeFile oldPath+ Directory.copyFile oldPath newPath+ Directory.removeFile oldPath else ioError ioe) {-| Create a directory@@ -1011,41 +1090,88 @@ Fails if the directory is present -} mkdir :: MonadIO io => FilePath -> io ()-mkdir path = liftIO (Filesystem.createDirectory False path)+mkdir path = liftIO (Directory.createDirectory 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)+mktree path = liftIO (Directory.createDirectoryIfMissing True path) -- | Copy a file cp :: MonadIO io => FilePath -> FilePath -> io ()-cp oldPath newPath = liftIO (Filesystem.copyFile oldPath newPath)+cp oldPath newPath = liftIO (Directory.copyFile oldPath newPath) --- | Copy a directory tree+#if !defined(mingw32_HOST_OS)+-- | Create a symlink from one @FilePath@ to another+symlink :: MonadIO io => FilePath -> FilePath -> io ()+symlink a b = liftIO $ createSymbolicLink (fp2fp a) (fp2fp b)+ where+ fp2fp = unpack . format fp + +#endif++{-| Returns `True` if the given `FilePath` is not a symbolic link++ This comes in handy in conjunction with `lsif`:++ > lsif isNotSymbolicLink+-}+isNotSymbolicLink :: MonadIO io => FilePath -> io Bool+isNotSymbolicLink = fmap (not . PosixCompat.isSymbolicLink) . lstat++-- | Copy a directory tree and preserve symbolic links cptree :: MonadIO io => FilePath -> FilePath -> io () cptree oldTree newTree = sh (do- oldPath <- lstree oldTree+ oldPath <- lsif isNotSymbolicLink oldTree+ -- The `system-filepath` library treats a path like "/tmp" as a file and not -- a directory and fails to strip it as a prefix from `/tmp/foo`. Adding -- `(</> "")` to the end of the path makes clear that the path is a -- directory- Just suffix <- return (Filesystem.stripPrefix (oldTree </> "") oldPath)+ Just suffix <- return (Internal.stripPrefix (oldTree <> [ FilePath.pathSeparator ]) oldPath)+ let newPath = newTree </> suffix+ isFile <- testfile oldPath++ fileStatus <- lstat oldPath++ if PosixCompat.isSymbolicLink fileStatus+ then do+ oldTarget <- liftIO (PosixCompat.readSymbolicLink oldPath)++ mktree (FilePath.takeDirectory newPath)++ liftIO (PosixCompat.createSymbolicLink oldTarget newPath)+ else if isFile+ then do+ mktree (FilePath.takeDirectory newPath)++ cp oldPath newPath+ else do+ mktree newPath )++-- | Copy a directory tree and dereference symbolic links+cptreeL :: MonadIO io => FilePath -> FilePath -> io ()+cptreeL oldTree newTree = sh (do+ oldPath <- lstree oldTree+ Just suffix <- return (Internal.stripPrefix (oldTree ++ "/") oldPath)+ let newPath = newTree </> suffix+ isFile <- testfile oldPath if isFile- then mktree (Filesystem.directory newPath) >> cp oldPath newPath+ then mktree (FilePath.takeDirectory newPath) >> cp oldPath newPath else mktree newPath ) + -- | Remove a file rm :: MonadIO io => FilePath -> io ()-rm path = liftIO (Filesystem.removeFile path)+rm path = liftIO (Directory.removeFile path) -- | Remove a directory rmdir :: MonadIO io => FilePath -> io ()-rmdir path = liftIO (Filesystem.removeDirectory path)+rmdir path = liftIO (Directory.removeDirectory path) {-| Remove a directory tree (equivalent to @rm -r@) @@ -1069,11 +1195,11 @@ -- | Check if a file exists testfile :: MonadIO io => FilePath -> io Bool-testfile path = liftIO (Filesystem.isFile path)+testfile path = liftIO (Directory.doesFileExist path) -- | Check if a directory exists testdir :: MonadIO io => FilePath -> io Bool-testdir path = liftIO (Filesystem.isDirectory path)+testdir path = liftIO (Directory.doesDirectoryExist path) -- | Check if a path exists testpath :: MonadIO io => FilePath -> io Bool@@ -1094,7 +1220,7 @@ #ifdef mingw32_HOST_OS then do handle <- Win32.createFile- (Filesystem.encodeString file)+ file Win32.gENERIC_WRITE Win32.fILE_SHARE_NONE Nothing@@ -1103,17 +1229,70 @@ Nothing (creationTime, _, _) <- Win32.getFileTime handle systemTime <- Win32.getSystemTimeAsFileTime- Win32.setFileTime handle creationTime systemTime systemTime+ Win32.setFileTime handle (Just creationTime) (Just systemTime) (Just systemTime) #else- then touchFile (Filesystem.encodeString file)+ then touchFile file #endif else output file empty ) +{-| This type is the same as @"System.Directory".`Directory.Permissions`@+ type except combining the `Directory.executable` and+ `Directory.searchable` fields into a single `executable` field for+ consistency with the Unix @chmod@. This simplification is still entirely+ consistent with the behavior of "System.Directory", which treats the two+ fields as interchangeable.+-}+data Permissions = Permissions+ { _readable :: Bool+ , _writable :: Bool+ , _executable :: Bool+ } deriving (Eq, Read, Ord, Show)++{-| Under the hood, "System.Directory" does not distinguish between+ `Directory.executable` and `Directory.searchable`. They both+ translate to the same `System.Posix.ownerExecuteMode` permission. That+ means that we can always safely just set the `Directory.executable`+ field and safely leave the `Directory.searchable` field as `False`+ because the two fields are combined with (`||`) to determine whether to set+ the executable bit.+-}+toSystemDirectoryPermissions :: Permissions -> Directory.Permissions+toSystemDirectoryPermissions p =+ ( Directory.setOwnerReadable (_readable p)+ . Directory.setOwnerWritable (_writable p)+ . Directory.setOwnerExecutable (_executable p)+ ) Directory.emptyPermissions++fromSystemDirectoryPermissions :: Directory.Permissions -> Permissions+fromSystemDirectoryPermissions p = Permissions+ { _readable = Directory.readable p+ , _writable = Directory.writable p+ , _executable =+ Directory.executable p || Directory.searchable p+ }+ {-| 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-w foo.txt+> chmod rwo "foo.txt" -- chmod u=rw foo.txt+> chmod executable "foo.txt" -- chmod u+x foo.txt+> chmod nonwritable "foo.txt" -- chmod u-w foo.txt++ The meaning of each permission is:++ * `readable` (@+r@ for short): For files, determines whether you can read+ from that file (such as with `input`). For directories, determines+ whether or not you can list the directory contents (such as with `ls`).+ Note: if a directory is not readable then `ls` will stream an empty list+ of contents++ * `writable` (@+w@ for short): For files, determines whether you can write+ to that file (such as with `output`). For directories, determines whether+ you can create a new file underneath that directory.++ * `executable` (@+x@ for short): For files, determines whether or not that+ file is executable (such as with `proc`). For directories, determines+ whether or not you can read or execute files underneath that directory+ (such as with `input` or `proc`) -} chmod :: MonadIO io@@ -1124,67 +1303,66 @@ -> io Permissions -- ^ Updated permissions chmod modifyPermissions path = liftIO (do- let path' = deslash (Filesystem.encodeString path)+ let path' = deslash path permissions <- Directory.getPermissions path'- let permissions' = modifyPermissions permissions- changed = permissions /= permissions'- when changed (Directory.setPermissions path' permissions')- return permissions' )+ let permissions' = fromSystemDirectoryPermissions permissions+ let permissions'' = modifyPermissions permissions'+ changed = permissions' /= permissions''+ let permissions''' = toSystemDirectoryPermissions 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' )+ let path' = deslash path+ permissions <- Directory.getPermissions path'+ return (fromSystemDirectoryPermissions permissions)) -- | 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 )+ let path' = deslash path+ Directory.setPermissions path' (toSystemDirectoryPermissions permissions) ) -- | Copy a file or directory's permissions (analogous to @chmod --reference@) copymod :: MonadIO io => FilePath -> FilePath -> io () copymod sourcePath targetPath = liftIO (do- let sourcePath' = deslash (Filesystem.encodeString sourcePath)- targetPath' = deslash (Filesystem.encodeString targetPath)+ let sourcePath' = deslash sourcePath+ targetPath' = deslash targetPath Directory.copyPermissions sourcePath' targetPath' ) -- | @+r@ readable :: Permissions -> Permissions-readable = Directory.setOwnerReadable True+readable p = p { _readable = True } -- | @-r@ nonreadable :: Permissions -> Permissions-nonreadable = Directory.setOwnerReadable False+nonreadable p = p { _readable = False } -- | @+w@ writable :: Permissions -> Permissions-writable = Directory.setOwnerWritable True+writable p = p { _writable = True } -- | @-w@ nonwritable :: Permissions -> Permissions-nonwritable = Directory.setOwnerWritable False+nonwritable p = p { _writable = False } -- | @+x@ executable :: Permissions -> Permissions-executable = Directory.setOwnerExecutable True+executable p = p { _executable = 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+nonexecutable p = p { _executable = False } -- | @-r -w -x@ ooo :: Permissions -> Permissions-ooo = const Directory.emptyPermissions+ooo _ = Permissions+ { _readable = False+ , _writable = False+ , _executable = False+ } -- | @+r -w -x@ roo :: Permissions -> Permissions@@ -1198,10 +1376,6 @@ 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@@ -1210,10 +1384,6 @@ 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@@ -1222,10 +1392,6 @@ rwx :: Permissions -> Permissions rwx = readable . writable . executable . ooo --- | @+r +w +s@-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@@ -1251,20 +1417,21 @@ whichAll :: FilePath -> Shell FilePath whichAll cmd = do Just paths <- need "PATH"- path <- select (Filesystem.splitSearchPathString . Text.unpack $ paths)+ path <- select (fmap Text.unpack (Text.splitOn ":" paths)) let path' = path </> cmd True <- testfile path' - let handler :: IOError -> IO Permissions+ let handler :: IOError -> IO Bool handler e = if isPermissionError e || isDoesNotExistError e- then return Directory.emptyPermissions+ then return False else throwIO e - perms <- liftIO (getmod path' `catchIOError` handler)+ let getIsExecutable = fmap _executable (getmod path')+ isExecutable <- liftIO (getIsExecutable `catchIOError` handler) - guard (Directory.executable perms)+ guard isExecutable return path' {-| Sleep for the given duration@@ -1323,10 +1490,8 @@ -- ^ Directory name template -> managed FilePath mktempdir parent prefix = using (do- let parent' = Filesystem.encodeString parent let prefix' = unpack prefix- dir' <- managed (withTempDirectory parent' prefix')- return (Filesystem.decodeString dir'))+ managed (withTempDirectory parent prefix')) {-| Create a temporary file underneath the given directory @@ -1345,11 +1510,10 @@ -- ^ File name template -> managed (FilePath, Handle) mktemp parent prefix = using (do- let parent' = Filesystem.encodeString parent let prefix' = unpack prefix (file', handle) <- managed (\k ->- withTempFile parent' prefix' (\file' handle -> k (file', handle)) )- return (Filesystem.decodeString file', handle) )+ withTempFile parent prefix' (\file' handle -> k (file', handle)) )+ return (file', handle) ) {-| Create a temporary file underneath the given directory @@ -1363,12 +1527,11 @@ -- ^ File name template -> managed FilePath mktempfile parent prefix = using (do- let parent' = Filesystem.encodeString parent let prefix' = unpack prefix (file', handle) <- managed (\k ->- withTempFile parent' prefix' (\file' handle -> k (file', handle)) )+ withTempFile parent prefix' (\file' handle -> k (file', handle)) ) liftIO (hClose handle)- return (Filesystem.decodeString file') )+ return file' ) -- | Fork a thread, acquiring an `Async` value fork :: MonadManaged managed => IO a -> managed (Async a)@@ -1390,8 +1553,7 @@ -- | Read lines of `Text` from a `Handle` inhandle :: Handle -> Shell Line-inhandle handle = Shell (\(FoldM step begin done) -> do- x0 <- begin+inhandle handle = Shell (\(FoldShell step begin done) -> do let loop x = do eof <- IO.hIsEOF handle if eof@@ -1400,7 +1562,7 @@ txt <- Text.hGetLine handle x' <- step x (unsafeTextToLine txt) loop $! x'- loop $! x0 )+ loop $! begin ) -- | Stream lines of `Text` to standard output stdout :: MonadIO io => Shell Line -> io ()@@ -1440,27 +1602,31 @@ -- | Acquire a `Managed` read-only `Handle` from a `FilePath` readonly :: MonadManaged managed => FilePath -> managed Handle-readonly file = using (managed (Filesystem.withTextFile file IO.ReadMode))+readonly file = using (managed (IO.withFile file IO.ReadMode)) -- | Acquire a `Managed` write-only `Handle` from a `FilePath` writeonly :: MonadManaged managed => FilePath -> managed Handle-writeonly file = using (managed (Filesystem.withTextFile file IO.WriteMode))+writeonly file = using (managed (IO.withFile file IO.WriteMode)) -- | Acquire a `Managed` append-only `Handle` from a `FilePath` appendonly :: MonadManaged managed => FilePath -> managed Handle-appendonly file = using (managed (Filesystem.withTextFile file IO.AppendMode))+appendonly file = using (managed (IO.withFile file IO.AppendMode)) -- | Combine the output of multiple `Shell`s, in order cat :: [Shell a] -> Shell a cat = msum +grepWith :: (b -> Text) -> Pattern a -> Shell b -> Shell b+grepWith f pattern' = mfilter (not . null . match pattern' . f)+ -- | Keep all lines that match the given `Pattern` grep :: Pattern a -> Shell Line -> Shell Line-grep pattern s = do- line <- s- _:_ <- return (match pattern (lineToText line))- return line+grep = grepWith lineToText +-- | Keep every `Text` element that matches the given `Pattern`+grepText :: Pattern a -> Shell Text -> Shell Text+grepText = grepWith id+ {-| Replace all occurrences of a `Pattern` with its `Text` result `sed` performs substitution on a line-by-line basis, meaning that@@ -1474,43 +1640,88 @@ necessarily incomplete. -} sed :: Pattern Text -> Shell Line -> Shell Line-sed pattern s = do- when (matchesEmpty pattern) (die message)- let pattern' = fmap Text.concat- (many (pattern <|> fmap Text.singleton anyChar))+sed pattern' s = do+ when (matchesEmpty pattern') (die message)+ let pattern'' = fmap Text.concat+ (many (pattern' <|> fmap Text.singleton anyChar)) line <- s- txt':_ <- return (match pattern' (lineToText line))+ txt':_ <- return (match pattern'' (lineToText line)) select (textToLines txt') where message = "sed: the given pattern matches the empty string" matchesEmpty = not . null . flip match "" +{-| Like `sed`, but the provided substitution must match the beginning of the+ line+-}+sedPrefix :: Pattern Text -> Shell Line -> Shell Line+sedPrefix pattern' s = do+ line <- s+ txt':_ <- return (match ((pattern' <> chars) <|> chars) (lineToText line))+ select (textToLines txt')++-- | Like `sed`, but the provided substitution must match the end of the line+sedSuffix :: Pattern Text -> Shell Line -> Shell Line+sedSuffix pattern' s = do+ line <- s+ txt':_ <- return (match ((chars <> pattern') <|> chars) (lineToText line))+ select (textToLines txt')++-- | Like `sed`, but the provided substitution must match the entire line+sedEntire :: Pattern Text -> Shell Line -> Shell Line+sedEntire pattern' s = do+ line <- s+ txt':_ <- return (match (pattern' <|> chars)(lineToText line))+ select (textToLines txt')+ -- | Make a `Shell Text -> Shell Text` function work on `FilePath`s instead. -- | Ignores any paths which cannot be decoded as valid `Text`. onFiles :: (Shell Text -> Shell Text) -> Shell FilePath -> Shell FilePath-onFiles f = fmap Filesystem.fromText . f . getRights . fmap Filesystem.toText- where- getRights :: forall a. Shell (Either a Text) -> Shell Text- getRights s = s >>= either (const empty) return-+onFiles f = fmap Text.unpack . f . fmap Text.pack -- | Like `sed`, but operates in place on a `FilePath` (analogous to @sed -i@) inplace :: MonadIO io => Pattern Text -> FilePath -> io ()-inplace pattern file = liftIO (runManaged (do- here <- pwd+inplace = update . sed++-- | Like `sedPrefix`, but operates in place on a `FilePath`+inplacePrefix :: MonadIO io => Pattern Text -> FilePath -> io ()+inplacePrefix = update . sedPrefix++-- | Like `sedSuffix`, but operates in place on a `FilePath`+inplaceSuffix :: MonadIO io => Pattern Text -> FilePath -> io ()+inplaceSuffix = update . sedSuffix++-- | Like `sedEntire`, but operates in place on a `FilePath`+inplaceEntire :: MonadIO io => Pattern Text -> FilePath -> io ()+inplaceEntire = update . sedEntire++{-| Update a file in place using a `Shell` transformation++ For example, this is used to implement the @inplace*@ family of utilities+-}+update :: MonadIO io => (Shell Line -> Shell Line) -> FilePath -> io ()+update f file = liftIO (runManaged (do+ here <- pwd+ (tmpfile, handle) <- mktemp here "turtle"- outhandle handle (sed pattern (input file))++ outhandle handle (f (input file))+ liftIO (hClose handle)+ copymod file tmpfile- mv tmpfile file )) + mv tmpfile file )) -- | Search a directory recursively for all files matching the given `Pattern` find :: Pattern a -> FilePath -> Shell FilePath-find pattern dir = do+find pattern' dir = do path <- lsif isNotSymlink dir- Right txt <- return (Filesystem.toText path)- _:_ <- return (match pattern txt)++ let txt = Text.pack path++ _:_ <- return (match pattern' txt)+ return path where isNotSymlink :: FilePath -> IO Bool@@ -1518,23 +1729,52 @@ file_stat <- lstat file return (not (PosixCompat.isSymbolicLink file_stat)) +-- | Filter a shell of FilePaths according to a given pattern+findtree :: Pattern a -> Shell FilePath -> Shell FilePath+findtree pat files = do+ path <- files++ let txt = Text.pack path++ _:_ <- return (match pat txt)++ return path++{- | Check if a file was last modified after a given+ timestamp+-} +cmin :: MonadIO io => UTCTime -> FilePath -> io Bool+cmin t file = do+ status <- lstat file+ return (adapt status)+ where+ adapt x = posixSecondsToUTCTime (modificationTime x) > t++{- | Check if a file was last modified before a given+ timestamp+-} +cmax :: MonadIO io => UTCTime -> FilePath -> io Bool+cmax t file = do+ status <- lstat file+ return (adapt status)+ where+ adapt x = posixSecondsToUTCTime (modificationTime x) < t+ -- | A Stream of @\"y\"@s yes :: Shell Line yes = fmap (\_ -> "y") endless -- | Number each element of a `Shell` (starting at 0) nl :: Num n => Shell a -> Shell (n, a)-nl s = Shell _foldIO'+nl s = Shell _foldShell' where- _foldIO' (FoldM step begin done) = _foldIO s (FoldM step' begin' done')+ _foldShell' (FoldShell step begin done) = _foldShell s (FoldShell step' begin' done') where step' (x, n) a = do x' <- step x (n, a) let n' = n + 1 n' `seq` return (x', n')- begin' = do- x0 <- begin- return (x0, 0)+ begin' = (begin, 0) done' (x, _) = done x data ZipState a b = Empty | HasA a | HasAB a b | Done@@ -1545,14 +1785,12 @@ truncated -} paste :: Shell a -> Shell b -> Shell (a, b)-paste sA sB = Shell _foldIOAB+paste sA sB = Shell _foldShellAB where- _foldIOAB (FoldM stepAB beginAB doneAB) = do- x0 <- beginAB-+ _foldShellAB (FoldShell stepAB beginAB doneAB) = do tvar <- STM.atomically (STM.newTVar Empty) - let begin = return ()+ let begin = () let stepA () a = STM.atomically (do x <- STM.readTVar tvar@@ -1566,7 +1804,7 @@ Empty -> STM.writeTVar tvar Done Done -> return () _ -> STM.retry )- let foldA = FoldM stepA begin doneA+ let foldA = FoldShell stepA begin doneA let stepB () b = STM.atomically (do x <- STM.readTVar tvar@@ -1580,10 +1818,10 @@ HasA _ -> STM.writeTVar tvar Done Done -> return () _ -> STM.retry )- let foldB = FoldM stepB begin doneB+ let foldB = FoldShell stepB begin doneB - withAsync (foldIO sA foldA) (\asyncA -> do- withAsync (foldIO sB foldB) (\asyncB -> do+ withAsync (_foldShell sA foldA) (\asyncA -> do+ withAsync (_foldShell sB foldB) (\asyncB -> do let loop x = do y <- STM.atomically (do z <- STM.readTVar tvar@@ -1598,29 +1836,32 @@ Just ab -> do x' <- stepAB x ab loop $! x'- x' <- loop $! x0+ x' <- loop $! beginAB wait asyncA wait asyncB doneAB x' ) ) -- | A `Shell` that endlessly emits @()@ endless :: Shell ()-endless = Shell (\(FoldM step begin _) -> do- x0 <- begin+endless = Shell (\(FoldShell step begin _) -> do let loop x = do x' <- step x () loop $! x'- loop $! x0 )+ loop $! begin ) --- | Limit a `Shell` to a fixed number of values+{-| Limit a `Shell` to a fixed number of values++ NOTE: This is not lazy and will still consume the entire input stream.+ There is no way to implement a lazy version of this utility.+-} limit :: Int -> Shell a -> Shell a-limit n s = Shell (\(FoldM step begin done) -> do+limit n s = Shell (\(FoldShell 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) )+ _foldShell s (FoldShell step' begin done) ) {-| Limit a `Shell` to values that satisfy the predicate @@ -1628,14 +1869,14 @@ predicate -} limitWhile :: (a -> Bool) -> Shell a -> Shell a-limitWhile predicate s = Shell (\(FoldM step begin done) -> do+limitWhile predicate s = Shell (\(FoldShell 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) )+ _foldShell s (FoldShell 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@@ -1684,7 +1925,7 @@ -- | Split a line into chunks delimited by the given `Pattern` cut :: Pattern a -> Text -> [Text]-cut pattern txt = head (match (selfless chars `sepBy` pattern) txt)+cut pattern' txt = head (match (selfless chars `sepBy` pattern') txt) -- This `head` should be safe ... in theory -- | Get the current time@@ -1693,7 +1934,7 @@ -- | Get the time a file was last modified datefile :: MonadIO io => FilePath -> io UTCTime-datefile path = liftIO (Filesystem.getModified path)+datefile path = liftIO (Directory.getModificationTime path) -- | Get the size of a file or a directory du :: MonadIO io => FilePath -> io Size@@ -1705,9 +1946,9 @@ let sizes = do child <- lstree path True <- testfile child- liftIO (Filesystem.getSize child)+ liftIO (Directory.getFileSize child) fold sizes Control.Foldl.sum- else Filesystem.getSize path+ else Directory.getFileSize path return (Size size) ) {-| An abstract file size@@ -1750,41 +1991,147 @@ then format (d%"."%d%" GB") remainingGigabytes remainingMegabytes else format (d%"."%d%" TB") numTerabytes remainingGigabytes ) +{-| Construct a 'Size' from an integer in bytes++>>> format sz (B 42)+"42 B"+-}+pattern B :: Integral n => n -> Size+pattern B { bytes } <- (fromInteger . _bytes -> bytes)+ where+ B = fromIntegral+{-# COMPLETE B #-}++{-| Construct a 'Size' from an integer in kilobytes++>>> format sz (KB 42)+"42.0 KB"+>>> let B n = KB 1 in n+1000+-}+pattern KB :: Integral n => n -> Size+pattern KB { kilobytes } <- (\(B x) -> x `div` 1000 -> kilobytes)+ where+ KB = B . (* 1000)+{-# COMPLETE KB #-}++{-| Construct a 'Size' from an integer in megabytes++>>> format sz (MB 42)+"42.0 MB"+>>> let KB n = MB 1 in n+1000+-}+pattern MB :: Integral n => n -> Size+pattern MB { megabytes } <- (\(KB x) -> x `div` 1000 -> megabytes)+ where+ MB = KB . (* 1000)+{-# COMPLETE MB #-}++{-| Construct a 'Size' from an integer in gigabytes++>>> format sz (GB 42)+"42.0 GB"+>>> let MB n = GB 1 in n+1000+-}+pattern GB :: Integral n => n -> Size+pattern GB { gigabytes } <- (\(MB x) -> x `div` 1000 -> gigabytes)+ where+ GB = MB . (* 1000)+{-# COMPLETE GB #-}++{-| Construct a 'Size' from an integer in terabytes++>>> format sz (TB 42)+"42.0 TB"+>>> let GB n = TB 1 in n+1000+-}+pattern TB :: Integral n => n -> Size+pattern TB { terabytes } <- (\(GB x) -> x `div` 1000 -> terabytes)+ where+ TB = GB . (* 1000)+{-# COMPLETE TB #-}++{-| Construct a 'Size' from an integer in kibibytes++>>> format sz (KiB 42)+"43.8 KB"+>>> let B n = KiB 1 in n+1024+-}+pattern KiB :: Integral n => n -> Size+pattern KiB { kibibytes } <- (\(B x) -> x `div` 1024 -> kibibytes)+ where+ KiB = B . (* 1024)+{-# COMPLETE KiB #-}++{-| Construct a 'Size' from an integer in mebibytes++>>> format sz (MiB 42)+"44.40 MB"+>>> let KiB n = MiB 1 in n+1024+-}+pattern MiB :: Integral n => n -> Size+pattern MiB { mebibytes } <- (\(KiB x) -> x `div` 1024 -> mebibytes)+ where+ MiB = KiB . (* 1024)+{-# COMPLETE MiB #-}++{-| Construct a 'Size' from an integer in gibibytes++>>> format sz (GiB 42)+"45.97 GB"+>>> let MiB n = GiB 1 in n+1024+-}+pattern GiB :: Integral n => n -> Size+pattern GiB { gibibytes } <- (\(MiB x) -> x `div` 1024 -> gibibytes)+ where+ GiB = MiB . (* 1024)+{-# COMPLETE GiB #-}++{-| Construct a 'Size' from an integer in tebibytes++>>> format sz (TiB 42)+"46.179 TB"+>>> let GiB n = TiB 1 in n+1024+-}+pattern TiB :: Integral n => n -> Size+pattern TiB { tebibytes } <- (\(GiB x) -> x `div` 1024 -> tebibytes)+ where+ TiB = GiB . (* 1024)+{-# COMPLETE TiB #-}+ -- | 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@) @@ -1817,7 +2164,7 @@ -- | Get the status of a file stat :: MonadIO io => FilePath -> io PosixCompat.FileStatus-stat = liftIO . PosixCompat.getFileStatus . Filesystem.encodeString+stat = liftIO . PosixCompat.getFileStatus -- | Size of the file in bytes. Does not follow symlinks fileSize :: PosixCompat.FileStatus -> Size@@ -1837,7 +2184,7 @@ -- | Get the status of a file, but don't follow symbolic links lstat :: MonadIO io => FilePath -> io PosixCompat.FileStatus-lstat = liftIO . PosixCompat.getSymbolicLinkStatus . Filesystem.encodeString+lstat = liftIO . PosixCompat.getSymbolicLinkStatus data WithHeader a = Header a@@ -1851,7 +2198,7 @@ header :: Shell a -> Shell (WithHeader a) header (Shell k) = Shell k' where- k' (FoldM step begin done) = k (FoldM step' begin' done')+ k' (FoldShell step begin done) = k (FoldShell step' begin' done') where step' (Pair x Nothing ) a = do x' <- step x (Header a)@@ -1860,23 +2207,138 @@ x' <- step x (Row a b) return (Pair x' (Just a)) - begin' = do- x <- begin- return (Pair x Nothing)+ begin' = Pair begin Nothing done' (Pair x _) = done x --- | Returns the result of a 'Shell' that outputs a single--- line:+-- | Returns the result of a 'Shell' that outputs a single line.+-- Note that if no lines / more than 1 line is produced by the Shell, this function will `die` with an error message. -- -- > main = do--- > Just directory <- single (inshell "pwd" empty)+-- > directory <- single (inshell "pwd" empty) -- > print directory-single :: MonadIO io => Shell a -> io (Maybe a)+single :: MonadIO io => Shell a -> io a single s = do as <- fold s Control.Foldl.list case as of- [a] -> return (Just a)+ [a] -> return a _ -> do let msg = format ("single: expected 1 line of input but there were "%d%" lines of input") (length as) die msg++-- | Filter adjacent duplicate elements:+--+-- >>> view (uniq (select [1,1,2,1,3]))+-- 1+-- 2+-- 1+-- 3+uniq :: Eq a => Shell a -> Shell a+uniq = uniqOn id++-- | Filter adjacent duplicates determined after applying the function to the element:+--+-- >>> view (uniqOn fst (select [(1,'a'),(1,'b'),(2,'c'),(1,'d'),(3,'e')]))+-- (1,'a')+-- (2,'c')+-- (1,'d')+-- (3,'e')+uniqOn :: Eq b => (a -> b) -> Shell a -> Shell a+uniqOn f = uniqBy (\a a' -> f a == f a')++-- | Filter adjacent duplicate elements determined via the given function:+--+-- >>> view (uniqBy (==) (select [1,1,2,1,3]))+-- 1+-- 2+-- 1+-- 3+uniqBy :: (a -> a -> Bool) -> Shell a -> Shell a+uniqBy cmp s = Shell $ \(FoldShell step begin done) -> do+ let step' (x, Just a') a | cmp a a' = return (x, Just a)+ step' (x, _) a = (, Just a) <$> step x a+ begin' = (begin, Nothing)+ done' (x, _) = done x+ foldShell s (FoldShell step' begin' done')++-- | Return a new `Shell` that discards duplicates from the input `Shell`:+--+-- >>> view (nub (select [1, 1, 2, 3, 3, 4, 3]))+-- 1+-- 2+-- 3+-- 4+nub :: Ord a => Shell a -> Shell a+nub = nubOn id++-- | Return a new `Shell` that discards duplicates determined via the given function from the input `Shell`:+--+-- >>> view (nubOn id (select [1, 1, 2, 3, 3, 4, 3]))+-- 1+-- 2+-- 3+-- 4+nubOn :: Ord b => (a -> b) -> Shell a -> Shell a+nubOn f s = Shell $ \(FoldShell step begin done) -> do+ let step' (x, bs) a | Set.member (f a) bs = return (x, bs)+ | otherwise = (, Set.insert (f a) bs) <$> step x a+ begin' = (begin, Set.empty)+ done' (x, _) = done x+ foldShell s (FoldShell step' begin' done')++-- | Return a list of the sorted elements of the given `Shell`, keeping duplicates:+--+-- >>> sort (select [1,4,2,3,3,7])+-- [1,2,3,3,4,7]+sort :: (Functor io, MonadIO io, Ord a) => Shell a -> io [a]+sort = sortOn id++-- | Return a list of the elements of the given `Shell`, sorted after applying the given function and keeping duplicates:+--+-- >>> sortOn id (select [1,4,2,3,3,7])+-- [1,2,3,3,4,7]+sortOn :: (Functor io, MonadIO io, Ord b) => (a -> b) -> Shell a -> io [a]+sortOn f = sortBy (comparing f)++-- | Return a list of the elements of the given `Shell`, sorted by the given function and keeping duplicates:+--+-- >>> sortBy (comparing fst) (select [(1,'a'),(4,'b'),(2,'c'),(3,'d'),(3,'e'),(7,'f')])+-- [(1,'a'),(2,'c'),(3,'d'),(3,'e'),(4,'b'),(7,'f')]+sortBy :: (Functor io, MonadIO io) => (a -> a -> Ordering) -> Shell a -> io [a]+sortBy f s = List.sortBy f <$> fold s list++{-| Group an arbitrary stream of `Text` into newline-delimited `Line`s++>>> stdout (toLines ("ABC" <|> "DEF" <|> "GHI")+ABCDEFGHI+>>> stdout (toLines empty) -- Note that this always emits at least 1 `Line`++>>> stdout (toLines ("ABC\nDEF" <|> "" <|> "GHI\nJKL"))+ABC+DEFGHI+JKL+-}+toLines :: Shell Text -> Shell Line+toLines (Shell k) = Shell k'+ where+ k' (FoldShell step begin done) =+ k (FoldShell step' begin' done')+ where+ step' (Pair x prefix) text = do+ let suffix :| lines = Turtle.Line.textToLines text++ let line = prefix <> suffix++ let lines' = line :| lines++ x' <- foldM step x (NonEmpty.init lines')++ let prefix' = NonEmpty.last lines'++ return (Pair x' prefix')++ begin' = (Pair begin "")++ done' (Pair x prefix) = do+ x' <- step x prefix+ done x'
src/Turtle/Shell.hs view
@@ -1,4 +1,7 @@-{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -fno-warn-missing-methods #-} {-| You can think of `Shell` as @[]@ + `IO` + `Managed`. In fact, you can embed@@ -47,9 +50,8 @@ `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)+> _foldShell s (FoldShell step begin done) = do+> x' <- _foldShell s (FoldShell step begin return) > done x' ... which is a fancy way of saying that your `Shell` must call @\'begin\'@@@ -59,8 +61,13 @@ module Turtle.Shell ( -- * Shell Shell(..)+ , FoldShell(..)+ , _foldIO+ , _Shell , foldIO+ , foldShell , fold+ , reduce , sh , view @@ -68,12 +75,15 @@ , select , liftIO , using+ , fromIO ) where import Control.Applicative import Control.Monad (MonadPlus(..), ap)+import Control.Monad.Catch (MonadThrow(..), MonadCatch(..)) import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.Managed (MonadManaged(..), with)+import qualified Control.Monad.Fail as Fail import Control.Foldl (Fold(..), FoldM(..)) import qualified Control.Foldl as Foldl import Data.Foldable (Foldable)@@ -82,17 +92,70 @@ import Data.String (IsString(..)) import Prelude -- Fix redundant import warnings +{-| This is similar to @`Control.Foldl.FoldM` `IO`@ except that the @begin@+ field is pure++ This small difference is necessary to implement a well-behaved `MonadCatch`+ instance for `Shell`+-}+data FoldShell a b = forall x . FoldShell (x -> a -> IO x) x (x -> IO b)+ -- | 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 }+newtype Shell a = Shell { _foldShell:: forall r . FoldShell a r -> IO r } +data Maybe' a = Just' !a | Nothing'++translate :: FoldM IO a b -> FoldShell a b+translate (FoldM step begin done) = FoldShell step' Nothing' done'+ where+ step' Nothing' a = do+ x <- begin+ x' <- step x a+ return $! Just' x'+ step' (Just' x) a = do+ x' <- step x a+ return $! Just' x'++ done' Nothing' = do+ x <- begin+ done x+ done' (Just' x) = do+ done x+ -- | 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) +{-| Provided for backwards compatibility with versions of @turtle-1.4.*@ and+ older+-}+_foldIO :: Shell a -> FoldM IO a r -> IO r+_foldIO s foldM = _foldShell s (translate foldM)++-- | Provided for ease of migration from versions of @turtle-1.4.*@ and older+_Shell :: (forall r . FoldM IO a r -> IO r) -> Shell a+_Shell f = Shell (f . adapt)+ where+ adapt (FoldShell step begin done) = FoldM step (return begin) done++-- | Use a `FoldShell` to reduce the stream of @a@'s produced by a `Shell`+foldShell :: MonadIO io => Shell a -> FoldShell a b -> io b+foldShell s f = liftIO (_foldShell 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) +-- | Flipped version of 'fold'. Useful for reducing a stream of data+--+-- ==== __Example__+-- Sum a `Shell` of numbers:+--+-- >>> select [1, 2, 3] & reduce Fold.sum+-- 6+reduce :: MonadIO io => Fold a b -> Shell a -> io b+reduce = flip fold+ -- | Run a `Shell` to completion, discarding any unused values sh :: MonadIO io => Shell a -> io () sh s = fold s (pure ())@@ -104,34 +167,33 @@ liftIO (print x) ) instance Functor Shell where- fmap f s = Shell (\(FoldM step begin done) ->+ fmap f s = Shell (\(FoldShell step begin done) -> let step' x a = step x (f a)- in _foldIO s (FoldM step' begin done) )+ in _foldShell s (FoldShell 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' )+ return a = Shell (\(FoldShell step begin done) -> do+ x <- step begin 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) )+ m >>= f = Shell (\(FoldShell step0 begin0 done0) -> do+ let step1 x a = _foldShell (f a) (FoldShell step0 x return)+ _foldShell m (FoldShell step1 begin0 done0) ) - fail _ = mzero+#if!(MIN_VERSION_base(4,13,0))+ fail = Fail.fail+#endif instance Alternative Shell where- empty = Shell (\(FoldM _ begin done) -> do- x <- begin- done x )+ empty = Shell (\(FoldShell _ begin done) -> done begin) - s1 <|> s2 = Shell (\(FoldM step begin done) -> do- x <- _foldIO s1 (FoldM step begin return)- _foldIO s2 (FoldM step (return x) done) )+ s1 <|> s2 = Shell (\(FoldShell step begin done) -> do+ x <- _foldShell s1 (FoldShell step begin return)+ _foldShell s2 (FoldShell step x done) ) instance MonadPlus Shell where mzero = empty@@ -139,18 +201,30 @@ mplus = (<|>) instance MonadIO Shell where- liftIO io = Shell (\(FoldM step begin done) -> do- x <- begin- a <- io- x' <- step x a- done x' )+ liftIO io = Shell (\(FoldShell step begin done) -> do+ a <- io+ x <- step begin a+ done x ) instance MonadManaged Shell where- using resource = Shell (\(FoldM step begin done) -> do- x <- begin- x' <- with resource (step x)- done x' )+ using resource = Shell (\(FoldShell step begin done) -> do+ x <- with resource (step begin)+ done x ) +instance MonadThrow Shell where+ throwM e = Shell (\_ -> throwM e)++instance MonadCatch Shell where+ m `catch` k = Shell (\f-> _foldShell m f `catch` (\e -> _foldShell (k e) f))++instance Fail.MonadFail Shell where+ fail _ = mzero++#if __GLASGOW_HASKELL__ >= 804+instance Monoid a => Semigroup (Shell a) where+ (<>) = mappend+#endif+ instance Monoid a => Monoid (Shell a) where mempty = pure mempty mappend = liftA2 mappend@@ -167,9 +241,25 @@ -- | Convert a list to a `Shell` that emits each element of the list select :: Foldable f => f a -> Shell a-select as = Shell (\(FoldM step begin done) -> do- x0 <- begin+select as = Shell (\(FoldShell step begin done) -> do let step' a k x = do x' <- step x a k $! x'- Data.Foldable.foldr step' done as $! x0 )+ Data.Foldable.foldr step' done as $! begin )++-- | Convert an `IO` action that returns a `Maybe` into a `Shell`+fromIO :: IO (Maybe a) -> Shell a+fromIO io =+ Shell+ (\(FoldShell step begin done) -> do+ let loop x = do+ m <- io+ case m of+ Just a -> do+ x' <- step x a+ loop x'+ Nothing -> do+ done x++ loop begin+ )
src/Turtle/Tutorial.hs view
@@ -47,7 +47,7 @@ can install a Haskell compiler if one is not already present. If you are curious about how these two lines work, they are described here: - <https://github.com/commercialhaskell/stack/blob/master/doc/GUIDE.md#ghcrunghc>+ <https://github.com/commercialhaskell/stack/blob/master/doc/GUIDE.md#script-interpreter> If you want to make a Windows script independently executable outside of a Git Bash environment, you can either (A) compile the script into an@@ -116,6 +116,9 @@ -- * Conclusion -- $conclusion + -- * Nix scripts+ -- $nix+ -- * FAQ -- $faq ) where@@ -129,7 +132,7 @@ -- -- @ -- #!\/usr\/bin\/env stack--- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- \-\- stack \-\-resolver lts-10.2 script -- \ -- -- #!\/bin\/bash -- {-\# LANGUAGE OverloadedStrings \#-} --@@ -194,7 +197,7 @@ -- difference: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > -- #!/bin/bash -- > {-# LANGUAGE OverloadedStrings #-} --@@ -211,7 +214,7 @@ -- program this way instead: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -227,7 +230,7 @@ -- permits definitions. If you were to insert a statement at the top-level: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -244,7 +247,7 @@ -- command: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > -- #!/bin/bash -- > {-# LANGUAGE OverloadedStrings #-} --@@ -270,7 +273,7 @@ -- -- @ -- #!\/usr\/bin\/env stack--- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- \-\- stack \-\-resolver lts-10.2 script -- \ -- -- #!\/bin\/bash -- import Turtle --@@ -298,7 +301,7 @@ -- within a larger subroutine: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > -- #!/bin/bash -- > import Turtle --@@ -360,7 +363,7 @@ -- following script to find out what happens if we choose `echo` instead: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > import Turtle -- > @@ -567,7 +570,7 @@ -- Let's illustrate this by adding types to our original script: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > import Turtle -- > @@ -605,7 +608,7 @@ -- can define unadorned `Line` values at the top-level, as we saw previously: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -625,7 +628,7 @@ -- Let's test this out by providing an incorrect type for @\'str\'@: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -677,7 +680,7 @@ -- a string: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -713,7 +716,7 @@ -- -- @ -- #!\/usr\/bin\/env stack--- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- \-\- stack \-\-resolver lts-10.2 script -- \ -- -- #!\/bin\/bash -- {-\# LANGUAGE OverloadedStrings \#-} --@@ -758,7 +761,7 @@ -- -- @ -- #!\/usr\/bin\/env stack--- \-\- stack \-\-install-ghc runghc \-\-package turtle+-- \-\- stack \-\-resolver lts-10.2 script -- -- {-\# LANGUAGE OverloadedStrings \#-} -- @@ -879,9 +882,9 @@ -- > FilePath "/tmp/.X0-lock" -- > FilePath "/tmp/pulse-PKdhtXMmr18n" -- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv"--- > FilePath "/tmp/tracker-gabriel"+-- > FilePath "/tmp/tracker-gabriella" -- > FilePath "/tmp/pulse-PYi1hSlWgNj2"--- > FilePath "/tmp/orbit-gabriel"+-- > FilePath "/tmp/orbit-gabriella" -- > FilePath "/tmp/ssh-vREYGbWGpiCa" -- > FilePath "/tmp/.ICE-unix --@@ -988,9 +991,9 @@ -- > FilePath "/tmp/.X0-lock" -- > FilePath "/tmp/pulse-PKdhtXMmr18n" -- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv"--- > FilePath "/tmp/tracker-gabriel"+-- > FilePath "/tmp/tracker-gabriella" -- > FilePath "/tmp/pulse-PYi1hSlWgNj2"--- > FilePath "/tmp/orbit-gabriel"+-- > FilePath "/tmp/orbit-gabriella" -- > FilePath "/tmp/ssh-vREYGbWGpiCa" -- > FilePath "/tmp/.ICE-unix" -- > FilePath "/usr/lib"@@ -1031,7 +1034,7 @@ -- We can use `select` to implement loops within a `Shell`: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > -- #!/bin/bash -- > {-# LANGUAGE OverloadedStrings #-} --@@ -1053,6 +1056,34 @@ -- > (2,3) -- > (2,4) --+-- This is because `Shell` behaves like a list comprehension, running each+-- following command once for each element in the stream. This implies that+-- an `Shell` stream that produces 0 elements will short-circuit and prevent+-- subsequent commands from being run.+--+-- > -- This stream emits 0 elements but still has side effects+-- > inner :: Shell a+-- > inner = do+-- > x <- select [1, 2]+-- > y <- select [3, 4]+-- > liftIO (print (x, y))+-- > empty+-- > +-- > outer :: Shell ()+-- > outer = do+-- > inner+-- > liftIO (echo "This step will never run")+--+-- If you want to run a `Shell` stream just for its side effects, wrap the+-- `Shell` with `sh`. This ensures that you don't alter the surrounding+-- `Shell`'s control flow by unintentionally running subsequent commands zero+-- times or multiple times:+--+-- > outer :: Shell ()+-- > outer = do+-- > sh inner+-- > liftIO (echo "Now this step will exactly once")+-- -- 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): --@@ -1078,9 +1109,9 @@ -- > FilePath "/tmp/.X0-lock" -- > FilePath "/tmp/pulse-PKdhtXMmr18n" -- > FilePath "/tmp/pulse-xHYcZ3zmN3Fv"--- > FilePath "/tmp/tracker-gabriel"+-- > FilePath "/tmp/tracker-gabriella" -- > FilePath "/tmp/pulse-PYi1hSlWgNj2"--- > FilePath "/tmp/orbit-gabriel"+-- > FilePath "/tmp/orbit-gabriella" -- > FilePath "/tmp/ssh-vREYGbWGpiCa" -- > FilePath "/tmp/.ICE-unix" --@@ -1133,8 +1164,8 @@ -- @ -- 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 +-- htXMmr18n\",FilePath \"\/tmp\/pulse-xHYcZ3zmN3Fv\",FilePath \"\/tmp\/tracker-gabriella+-- \",FilePath \"\/tmp\/pulse-PYi1hSlWgNj2\",FilePath \"\/tmp\/orbit-gabriella\",FilePath -- \"\/tmp\/ssh-vREYGbWGpiCa\",FilePath \"\/tmp\/.ICE-unix\"] -- @ --@@ -1178,7 +1209,7 @@ -- to standard output: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > -- #!/bin/bash -- > {-# LANGUAGE OverloadedStrings #-} --@@ -1223,9 +1254,9 @@ -- > .X0-lock -- > pulse-PKdhtXMmr18n -- > pulse-xHYcZ3zmN3Fv--- > tracker-gabriel+-- > tracker-gabriella -- > pulse-PYi1hSlWgNj2--- > orbit-gabriel+-- > orbit-gabriella -- > ssh-vREYGbWGpiCa -- > .ICE-unix --@@ -1419,7 +1450,7 @@ -- a `Shell`: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -1445,7 +1476,7 @@ -- file and directory are still cleaned up correctly: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -1462,10 +1493,10 @@ -- $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:+-- If you are sick of having to 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@@ -1509,7 +1540,7 @@ -- to: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -1536,7 +1567,7 @@ -- positional arguments for the source and destination file, you can write: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > -- cp.hs -- >@@ -1550,7 +1581,7 @@ -- > <*> argPath "dest" "The destination file" -- > -- > main = do--- > (src, dest) <- options "A simple `cp` script" parser+-- > (src, dest) <- options "A simple `cp` utility" parser -- > cp src dest -- -- If you run the script without any arguments, you will get an auto-generated@@ -1601,7 +1632,7 @@ -- -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -1650,7 +1681,7 @@ -- as short-hands for the flags: -- -- > #!/usr/bin/env stack--- > -- stack --install-ghc runghc --package turtle+-- > -- stack --resolver lts-10.2 script -- > -- > {-# LANGUAGE OverloadedStrings #-} -- > @@ -1762,12 +1793,37 @@ -- 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>+-- <https://github.com/Gabriella439/turtle/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! +-- $nix+--+-- You can also turn `turtle` scripts into runnable scripts using Nix, like+-- this:+--+-- > #! /usr/bin/env nix-shell+-- > #! nix-shell -i runghc --packages "ghc.withPackages (x: [ x.turtle ])"+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > main = echo "Hello, world!"+--+-- ... or create an auto-reloading script like this:+--+-- > #! /usr/bin/env nix-shell+-- > #! nix-shell -i "ghcid -T main" --packages ghcid "ghc.withPackages (x: [ x.turtle ])"+-- > +-- > {-# LANGUAGE OverloadedStrings #-}+-- > +-- > import Turtle+-- > +-- > main = echo "Hello, world!"+ -- $faq -- -- These are the most frequently asked questions from new users:@@ -1818,3 +1874,11 @@ -- /Answer:/ Use `runManaged`, `sh`, or (`<|>`) (all resources acquired in the -- left stream will close before beginning the right stream). Alternatively, -- use `with` to acquire a resource for a limited scope.+--+-- /Question:/ How do I use @turtle@ to run another shell as a subprocess?+--+-- /Answer:/ Use `Turtle.system` in conjunction with the `process` library,+-- like this:+--+-- > Turtle.system (System.Process.proc "/bin/sh" []) empty+
test/cptree.hs view
@@ -1,12 +1,12 @@ {-# LANGUAGE OverloadedStrings #-} import Turtle-import Filesystem.Path.CurrentOS () import System.IO.Temp (withSystemTempDirectory)+import qualified Control.Monad.Fail as Fail import Control.Monad (unless) check :: String -> Bool-> IO ()-check errorMessage successs = unless successs $ fail errorMessage+check errorMessage successs = unless successs $ Fail.fail errorMessage main :: IO () main = withSystemTempDirectory "tempDir" (runTest . fromString)
+ test/system-filepath.hs view
@@ -0,0 +1,226 @@+{-# Language CPP #-}+{-# Options_GHC -Wno-deprecations #-}++module Main (main) where++import Test.Tasty+import Test.Tasty.HUnit+import Turtle++main :: IO ()+main = defaultMain $ testGroup "system-filepath tests"+ [ test_Root+ , test_Directory+ , test_Parent+ , test_CommonPrefix+ , test_StripPrefix+ , test_Collapse+ , test_Filename+ , test_Dirname+ , test_Basename+ , test_Absolute+ , test_Relative+ , test_SplitDirectories+ , test_SplitExtension+ ]++test_Root :: TestTree+test_Root = testCase "root" $ do+ "" @=? root ""+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+ "c:\\" @=? root "c:\\"+ "c:\\" @=? root "c:\\foo"+#else+ "/" @=? root "/"+ "/" @=? root "/foo"+#endif+ "" @=? root "foo"++test_Directory :: TestTree+test_Directory = testCase "directory" $ do+ "./" @=? directory ""+ "/" @=? directory "/"+ "/foo/" @=? directory "/foo/bar"+ "/foo/bar/" @=? directory "/foo/bar/"+ "./" @=? directory "."+ "../" @=? directory ".."+ "../" @=? directory "../foo"+ "../foo/" @=? directory "../foo/"+ "./" @=? directory "foo"+ "foo/" @=? directory "foo/bar"++test_Parent :: TestTree+test_Parent = testCase "parent" $ do+ -- The behavior in the presence of `.` / `..` is messed up, but that's how+ -- the old system-filepath package worked, so we're preserving that for+ -- backwards compatibility (for now)+ "./" @=? parent ""+ "./" @=? parent "."+ "./" @=? parent ".."+ "/" @=? parent "/.."+ "/" @=? parent "/."+ "./" @=? parent "./."+ "./" @=? parent "./.."+ "../" @=? parent "../.."+ "../" @=? parent "../."++#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+ "c:\\" @=? parent "c:\\"+#else+ "/" @=? parent "/"+#endif+ "./" @=? parent "foo"+ "./" @=? parent "./foo"+ "./foo/" @=? parent "foo/bar"+ "./foo/" @=? parent "foo/bar/"+ "./foo/" @=? parent "./foo/bar"+ "/" @=? parent "/foo"+ "/foo/" @=? parent "/foo/bar"++test_Filename :: TestTree+test_Filename = testCase "filename" $ do+ "" @=? filename ""+ "" @=? filename "."+ "" @=? filename ".."+ "" @=? filename "/"+ "" @=? filename "/foo/"+ "bar" @=? filename "/foo/bar"+ "bar.txt" @=? filename "/foo/bar.txt"++test_Dirname :: TestTree+test_Dirname = testCase "dirname" $ do+ "" @=? dirname ""+ "" @=? dirname "/"+ "" @=? dirname "foo"+ ".." @=? dirname ".."+ "foo" @=? dirname "foo/bar"+ "bar" @=? dirname "foo/bar/"+ "bar" @=? dirname "foo/bar/baz.txt"++ -- the directory name will be re-parsed to a file name.+ let dirnameExts q = snd (splitExtensions (dirname q))+ ["d"] @=? dirnameExts "foo.d/bar"++test_Basename :: TestTree+test_Basename = testCase "basename" $ do+ "" @=? basename ".."+ "" @=? basename "/"+ "" @=? basename "."+ ".txt" @=? basename ".txt"+ "foo" @=? basename "foo.txt"+ "bar" @=? basename "foo/bar.txt"++#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+ "bar" @=? basename "c:\\foo\\bar"+ "bar" @=? basename "c:\\foo\\bar.txt"+#else+ "bar" @=? basename "/foo/bar"+ "bar" @=? basename "/foo/bar.txt"+#endif++test_Absolute :: TestTree+test_Absolute = testCase "absolute" $ do+ let myAssert q = assertBool ("absolute " ++ show q) $ absolute q+ let myAssert' q = assertBool ("not $ absolute " ++ show q) $ not $ absolute q++#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+ myAssert "c:\\"+ myAssert "c:\\foo\\bar"+ myAssert' ""+ myAssert' "foo\\bar"+ myAssert' "\\foo\\bar"+#else+ myAssert "/"+ myAssert "/foo/bar"+ myAssert' ""+ myAssert' "foo/bar"+#endif+++test_Relative :: TestTree+test_Relative = testCase "relative" $ do+ let myAssert q = assertBool ("relative " ++ show q) $ relative q+ let myAssert' q = assertBool ("not $ relative " ++ show q) $ not $ relative q++#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+ myAssert' "c:\\"+ myAssert' "c:\\foo\\bar"+ myAssert ""+ myAssert "foo\\bar"+#else+ myAssert' "/"+ myAssert' "/foo/bar"+ myAssert ""+ myAssert "foo/bar"+#endif++test_CommonPrefix :: TestTree+test_CommonPrefix = testCase "commonPrefix" $ do+ "" @=? commonPrefix []+ "./" @=? commonPrefix [".", "."]+ "" @=? commonPrefix [".", ".."]+ "foo/" @=? commonPrefix ["foo/bar", "foo/baz"]+ "foo/a.b" @=? commonPrefix ["foo/a.b.c", "foo/a.b.d"]+ "" @=? commonPrefix ["foo/", "bar/"]++test_StripPrefix :: TestTree+test_StripPrefix = testCase "stripPrefix" $ do+ Just "" @=? stripPrefix "" ""+ Just "/" @=? stripPrefix "" "/"+ Just "" @=? stripPrefix "/" "/"+ Just "foo" @=? stripPrefix "/" "/foo"+ Just "foo" @=? stripPrefix "./" "./foo"+ Just "foo.ext" @=? stripPrefix "./" "./foo.ext"+ Just "foo/bar" @=? stripPrefix "/" "/foo/bar"+ Just "bar" @=? stripPrefix "/foo/" "/foo/bar"+ Just "bar/baz" @=? stripPrefix "/foo/" "/foo/bar/baz"+ Just ".txt" @=? stripPrefix "/foo/bar" "/foo/bar.txt"+ Just ".gz" @=? stripPrefix "/foo/bar.txt" "/foo/bar.txt.gz"++ -- Test ignoring non-matching prefixes+ Nothing @=? stripPrefix "/foo" "/foo/bar"+ Nothing @=? stripPrefix "/foo/bar/baz" "/foo"+ Nothing @=? stripPrefix "/foo/baz/" "/foo/bar/qux"+ Nothing @=? stripPrefix "/foo/bar/baz" "/foo/bar/qux"++test_Collapse :: TestTree+test_Collapse = testCase "collapse" $ do+ -- This behavior differs from the old `system-filepath` package, but this+ -- behavior is more correct in the presence of symlinks+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+ "foo\\..\\bar" @=? collapse "foo/../bar"+ "foo\\bar" @=? collapse "foo/bar"+ "foo\\bar" @=? collapse "foo/./bar"+#else+ "foo/../bar" @=? collapse "foo/../bar"+ "foo/bar" @=? collapse "foo/bar"+ "foo/bar" @=? collapse "foo/./bar"+#endif++test_SplitDirectories :: TestTree+test_SplitDirectories = testCase "splitDirectories" $ do+ [] @=? splitDirectories ""+ ["./"] @=? splitDirectories "."+ ["../"] @=? splitDirectories ".."+ ["foo/", "../"] @=? splitDirectories "foo/.."+ ["foo/", "./"] @=? splitDirectories "foo/."+ ["/"] @=? splitDirectories "/"+ ["/", "a"] @=? splitDirectories "/a"+ ["/", "ab/", "cd"] @=? splitDirectories "/ab/cd"+ ["/", "ab/", "cd/"] @=? splitDirectories "/ab/cd/"+ ["ab/", "cd"] @=? splitDirectories "ab/cd"+ ["ab/", "cd/"] @=? splitDirectories "ab/cd/"+ ["ab/", "cd.txt"] @=? splitDirectories "ab/cd.txt"+ ["ab/", "cd/", ".txt"] @=? splitDirectories "ab/cd/.txt"+ ["ab/", "./", "cd"] @=? splitDirectories "ab/./cd"++test_SplitExtension :: TestTree+test_SplitExtension = testCase "splitExtension" $ do+ ("", Nothing) @=? splitExtension ""+ ("foo", Nothing) @=? splitExtension "foo"+ ("foo", Just "") @=? splitExtension "foo."+ ("foo", Just "a") @=? splitExtension "foo.a"+ ("foo.a/", Nothing) @=? splitExtension "foo.a/"+ ("foo.a/bar", Nothing) @=? splitExtension "foo.a/bar"+ ("foo.a/bar", Just "b") @=? splitExtension "foo.a/bar.b"+ ("foo.a/bar.b", Just "c") @=? splitExtension "foo.a/bar.b.c"
turtle.cabal view
@@ -1,14 +1,13 @@ Name: turtle-Version: 1.3.6+Version: 1.6.2 Cabal-Version: >=1.10 Build-Type: Simple License: BSD3 License-File: LICENSE-Copyright: 2015 Gabriel Gonzalez-Author: Gabriel Gonzalez-Maintainer: Gabriel439@gmail.com-Tested-With: GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.2, GHC == 8.0.1-Bug-Reports: https://github.com/Gabriel439/Haskell-Turtle-Library/issues+Copyright: 2015 Gabriella Gonzalez+Author: Gabriella Gonzalez+Maintainer: GenuineGabriella@gmail.com+Bug-Reports: https://github.com/Gabriella439/turtle/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@@ -30,7 +29,7 @@ . * Formatting: Type-safe @printf@-style text formatting .- * Modern: Supports @text@ and @system-filepath@+ * Modern: Supports @text@ . Read "Turtle.Tutorial" for a detailed tutorial or "Turtle.Prelude" for a quick-start guide@@ -40,40 +39,70 @@ then you should also check out the @Shelly@ library which provides similar functionality. Category: System++Tested-With:+ GHC == 9.6.1+ GHC == 9.4.4+ GHC == 9.2.7+ GHC == 9.0.2+ GHC == 8.10.7+ GHC == 8.8.4+ GHC == 8.6.5+ GHC == 8.4.4+ GHC == 8.2.2+ GHC == 8.0.2+ Extra-Source-Files: CHANGELOG.md+ Source-Repository head Type: git- Location: https://github.com/Gabriel439/Haskell-Turtle-Library+ Location: https://github.com/Gabriella439/turtle +Flag new-deps+ Description: Use new versions of ansi-wl-pprint and optparse-applicative+ Manual: False+ Default: True+ Library HS-Source-Dirs: src Build-Depends:- base >= 4.6 && < 5 ,- ansi-wl-pprint >= 0.6 && < 0.7 ,- async >= 2.0.0.0 && < 2.2 ,- bytestring >= 0.9.1.8 && < 0.11,- clock >= 0.4.1.2 && < 0.8 ,- directory >= 1.0.7 && < 1.4 ,- foldl >= 1.1 && < 1.4 ,+ -- 2021-09-07: Turtle.Prelude uses GHC-8.0 features, so base >= 4.9+ base >= 4.9 && < 5 ,+ async >= 2.0.0.0 && < 2.3 ,+ bytestring >= 0.9.1.8 && < 0.12,+ clock >= 0.4.1.2 && < 0.9 ,+ containers >= 0.5.0.0 && < 0.7 ,+ directory >= 1.3.1.0 && < 1.4 ,+ exceptions >= 0.4 && < 0.11,+ filepath >= 1.4.1.2 && < 1.5 ,+ foldl >= 1.1 && < 1.5 , hostname < 1.1 , managed >= 1.0.3 && < 1.1 , process >= 1.0.1.1 && < 1.7 ,- semigroups >= 0.5.0 && < 0.19,- system-filepath >= 0.3.1 && < 0.5 ,- system-fileio >= 0.2.1 && < 0.4 ,- stm < 2.5 ,- temporary < 1.3 ,- text < 1.3 ,- time < 1.9 ,- transformers >= 0.2.0.0 && < 0.6 ,- optparse-applicative >= 0.13 && < 0.15,+ stm < 2.6 ,+ streaming-commons < 0.3 ,+ temporary < 1.4 ,+ text >= 1.0.0 && < 2.1 ,+ time < 1.13,+ transformers >= 0.2.0.0 && < 0.7 , optional-args >= 1.0 && < 2.0 ,- unix-compat >= 0.4 && < 0.5+ unix-compat >= 0.4 && < 0.8 if os(windows)- Build-Depends: Win32 >= 2.2.0.1 && < 2.4+ Build-Depends: Win32 >= 2.12 else- Build-Depends: unix >= 2.5.1.0 && < 2.8+ Build-Depends: unix >= 2.5.1.0 && < 2.9++ -- A possible Cabal issue makes it try an old version of ansi-wl-pprint+ -- even though a newer would work.+ -- See discussion in https://github.com/Gabriella439/turtle/pull/446+ if flag(new-deps)+ Build-Depends: ansi-wl-pprint >= 1.0 && < 1.1 ,+ optparse-applicative >= 0.18 && < 0.19+ else+ Build-Depends: ansi-wl-pprint >= 0.6 && < 1.0 ,+ optparse-applicative >= 0.16 && < 0.18+ Exposed-Modules: Turtle, Turtle.Bytes,@@ -97,7 +126,7 @@ Default-Language: Haskell2010 Build-Depends: base >= 4 && < 5 ,- doctest >= 0.7 && < 0.12+ doctest >= 0.7 test-suite regression-broken-pipe Type: exitcode-stdio-1.0@@ -128,9 +157,22 @@ Build-Depends: base >= 4 && < 5, temporary,- system-filepath >= 0.4,+ filepath >= 0.4, turtle +test-suite system-filepath-tests+ Type: exitcode-stdio-1.0+ HS-Source-Dirs: test+ Main-Is: system-filepath.hs+ GHC-Options: -Wall -threaded+ Default-Language: Haskell2010+ Build-Depends:+ base,+ filepath,+ tasty >=1.4 && <1.5,+ tasty-hunit >=0.10 && <0.11,+ turtle+ benchmark bench Type: exitcode-stdio-1.0 HS-Source-Dirs: bench@@ -138,10 +180,7 @@ GHC-Options: -O2 -Wall -threaded Default-Language: Haskell2010 Build-Depends:- base >= 4 && < 5 ,- text < 1.3,+ base >= 4 && < 5 ,+ tasty-bench >= 0.3.1 ,+ text < 1.3, turtle- if impl(ghc < 7.8)- Build-Depends: criterion >= 0.4 && < 1.1.4.0- else- Build-Depends: criterion >= 0.4 && < 1.3